---
title: Mechanics
format:
html:
theme: flatly
# grid:
# body-width: 1000px # Domyślnie jest to ok. 800-900px
# margin-width: 250px # Szerokość bocznych paneli (np. TOC)
toc: true
toc-depth: 3
highlight-style: tango
code-line-numbers: true
code-fold: true
code-summary: "Show the code"
code-tools: true
code-block-bg: "rgba(42, 174, 42, 0.02)"
code-block-border-left: "#2aae2a"
code-language-label: true
css: styles.css
math: mathjax
self-contained: true
other-links:
- text: Main page
href: https://dchorazkiewicz.github.io/Mathematics_Physics_Lectures
---
## Description of motion
### Movement in 2D Cartesian coordinates
We could start considering moving particle in 1D but 2D examples are more interesting.
First we have to define the position of the particle in 2D Cartesian coordinates.
The position of the particle is given by the vector $\mathbf{r} = (x, y)$, where $x$ and $y$ are the coordinates of the particle in the $x$ and $y$ axes, respectively. If these coordinates are functions of time, we can write the position vector as $\mathbf{r}(t) = (x(t), y(t))$. This function describes the trajectory of the particle in the $xy$ plane.
$$
[a,b]\rightarrow \mathbb{R}^2:
t\rightarrow \mathbf{r}(t) = (x(t), y(t))
$$
#### Explanation of Symbols:
- $\mathbf{r}$: Position vector of the particle.
- $(x, y)$: Cartesian coordinates representing the particle's position in the $x$ and $y$ axes.
- $\mathbf{r}(t)$: Position vector as a function of time, describing the particle's motion.
- $x(t), y(t)$: Time-dependent functions representing the particle's coordinates in the $x$ and $y$ axes.
- $[a, b]$: Interval of time during which the particle's motion is considered.
- $\mathbb{R}^2$: Two-dimensional Cartesian coordinate space.
- $t$: Time variable, used as a parameter for the position function.
#### How Mathematicians Read This Notation:
Mathematicians interpret the notation as follows:
- "$[a,b] \rightarrow \mathbb{R}^2$" means "a mapping (or function) is defined from the interval $[a, b]$ in time to the two-dimensional Cartesian space $\mathbb{R}^2$."
- "$t \rightarrow \mathbf{r}(t) = (x(t), y(t))$" specifies that for each time $t$ within the interval $[a, b]$, there exists a corresponding position vector $\mathbf{r}(t)$ in $\mathbb{R}^2$, which consists of the time-dependent coordinates $x(t)$ and $y(t)$.
- The overall expression describes a trajectory as a continuous function of time, mapping the progression of time to the corresponding locations in 2D space.
#### Examples
Let us consider two examples of the particle motion in 2D Cartesian coordinates.
$$
\begin{align*}
\mathbf{r}_1(t) &= (t, -t^2 + t) \\
\mathbf{r}_2(t) &= (\cos(t), \sin(t))
\end{align*}
$$
##### Python implementation
```{python}
import numpy as np
import matplotlib.pyplot as plt
def r1(t):
return np.array([t, -t**2 + t])
def r2(t):
return np.array([2 * np.cos(t), 2 * np.sin(t)])
# Define the time ranges
t1 = np.linspace(0, 2, 100)
t2 = np.linspace(0, 2 * np.pi, 100)
# Create a side-by-side plot layout
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(12, 5)) # One row, two columns
# First plot
axes[0].plot(r1(t1)[0], r1(t1)[1])
axes[0].set_xlabel('x')
axes[0].set_ylabel('y')
axes[0].set_title('Plot 1: r1(t)')
# Second plot
axes[1].plot(r2(t2)[0], r2(t2)[1])
axes[1].set_xlabel('x')
axes[1].set_ylabel('y')
axes[1].set_title('Plot 2: r2(t)')
# Adjust aspect ratios if needed
axes[0].set_aspect('equal', 'box')
axes[1].set_aspect('equal', 'box')
# Optimize layout
plt.tight_layout()
plt.show()
```
##### HTML implementation
```{=html}
<div class="parametric-motion-widget">
<style>
.parametric-motion-widget {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
padding: 20px;
background-color: #ffffff;
border: 1px solid #e0e0e0;
border-radius: 8px;
color: #333;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
max-width: 800px;
margin: 20px auto;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
box-sizing: border-box;
}
.parametric-motion-widget h3 {
margin-top: 0;
color: #444;
font-size: 1.1em;
text-transform: uppercase;
letter-spacing: 1px;
}
.parametric-motion-widget .canvas-wrapper {
position: relative;
margin: 10px 0 20px 0;
background: #fdfdfd;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: inset 0 0 10px rgba(0,0,0,0.02);
}
.parametric-motion-widget canvas {
display: block;
cursor: crosshair;
}
.parametric-motion-widget .controls-panel {
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
background: #f5f5f5;
padding: 15px;
border-radius: 6px;
border: 1px solid #eee;
}
.parametric-motion-widget .control-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.parametric-motion-widget label {
font-weight: 600;
font-size: 0.85em;
color: #555;
display: flex;
justify-content: space-between;
}
.parametric-motion-widget select,
.parametric-motion-widget button {
padding: 8px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
background: white;
cursor: pointer;
transition: all 0.2s;
}
.parametric-motion-widget button {
font-weight: bold;
color: white;
border: none;
}
.parametric-motion-widget .btn-start { background-color: #2e7d32; }
.parametric-motion-widget .btn-start:hover { background-color: #1b5e20; }
.parametric-motion-widget .btn-pause { background-color: #f57c00; }
.parametric-motion-widget .btn-reset {
background-color: #607d8b;
margin-top: 5px;
}
.parametric-motion-widget .btn-reset:hover { background-color: #455a64; }
.parametric-motion-widget input[type=range] {
width: 100%;
cursor: pointer;
}
.parametric-motion-widget .math-info {
grid-column: 1 / -1;
background: white;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
text-align: center;
font-family: 'Courier New', monospace;
font-weight: bold;
color: #1565c0;
font-size: 1.1em;
}
.parametric-motion-widget .value-display {
color: #1565c0;
}
/* Responsiveness */
@media (max-width: 600px) {
.parametric-motion-widget .controls-panel {
grid-template-columns: 1fr;
}
}
</style>
<h3>Visualization of Position Vector r(t)</h3>
<div class="canvas-wrapper">
<canvas class="param-canvas" width="700" height="450"></canvas>
</div>
<div class="controls-panel">
<div class="math-info">r(t) = ...</div>
<div class="control-group">
<label>Select Trajectory:</label>
<select class="curve-select">
<option value="parabola">Parabola (Projectile)</option>
<option value="circle">Circle</option>
<option value="lissajous">Lissajous Curve</option>
<option value="spiral">Archimedean Spiral</option>
</select>
<button class="btn-start">Start</button>
<button class="btn-reset">Reset</button>
</div>
<div class="control-group">
<label>Time t: <span class="t-val value-display">0.00</span></label>
<input type="range" class="t-slider" min="0" max="1000" step="1" value="0">
<label style="margin-top: 10px;">Animation Speed:</label>
<input type="range" class="speed-slider" min=".5" max="3" step=".5" value="3">
</div>
</div>
<script>
(function() {
const widget = document.currentScript.parentElement;
const canvas = widget.querySelector('.param-canvas');
const ctx = canvas.getContext('2d');
// UI Elements
const curveSelect = widget.querySelector('.curve-select');
const btnStart = widget.querySelector('.btn-start');
const btnReset = widget.querySelector('.btn-reset');
const tSlider = widget.querySelector('.t-slider');
const tValDisplay = widget.querySelector('.t-val');
const speedSlider = widget.querySelector('.speed-slider');
const mathDisplay = widget.querySelector('.math-info');
let state = {
t: 0,
curveKey: 'parabola',
isRunning: false,
speed: 3,
animFrame: null
};
// Curve Configuration
// scale: pixels per logical unit
// origin: pixel coordinates of (0,0)
const curves = {
parabola: {
label: "r(t) = [ 2t, -t² + 4t ]",
fn: (t) => ({ x: 2*t, y: -t*t + 4*t }),
tMin: 0, tMax: 4.5,
scale: 60,
origin: { x: 50, y: 380 }, // bottom-left
gridStep: 1
},
circle: {
label: "r(t) = [ 3cos(t), 3sin(t) ]",
fn: (t) => ({ x: 3*Math.cos(t), y: 3*Math.sin(t) }),
tMin: 0, tMax: 2 * Math.PI,
scale: 50,
origin: { x: 350, y: 225 }, // center
gridStep: 1
},
lissajous: {
label: "r(t) = [ 4sin(3t), 3sin(2t) ]",
fn: (t) => ({ x: 4*Math.sin(3*t), y: 3*Math.sin(2*t) }),
tMin: 0, tMax: 2 * Math.PI,
scale: 45,
origin: { x: 350, y: 225 }, // center
gridStep: 1
},
spiral: {
label: "r(t) = [ 0.5t·cos(t), 0.5t·sin(t) ]",
fn: (t) => ({ x: 0.5*t*Math.cos(t), y: 0.5*t*Math.sin(t) }),
tMin: 0, tMax: 4 * Math.PI,
scale: 40,
origin: { x: 350, y: 225 },
gridStep: 1
}
};
function getConf() { return curves[state.curveKey]; }
// Logic -> Screen Conversion (Y flips: logic up is screen down)
function toScreen(lx, ly, conf) {
return {
x: conf.origin.x + lx * conf.scale,
y: conf.origin.y - ly * conf.scale
};
}
// Draw Arrow (Vector)
function drawArrow(ctx, fromX, fromY, toX, toY, color) {
const headlen = 12;
const dx = toX - fromX;
const dy = toY - fromY;
const angle = Math.atan2(dy, dx);
ctx.beginPath();
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.lineWidth = 2.5;
// Line
ctx.moveTo(fromX, fromY);
ctx.lineTo(toX, toY);
ctx.stroke();
// Arrow Head
ctx.beginPath();
ctx.moveTo(toX, toY);
ctx.lineTo(toX - headlen * Math.cos(angle - Math.PI / 6), toY - headlen * Math.sin(angle - Math.PI / 6));
ctx.lineTo(toX - headlen * Math.cos(angle + Math.PI / 6), toY - headlen * Math.sin(angle + Math.PI / 6));
ctx.lineTo(toX, toY);
ctx.fill();
}
function drawGrid(conf) {
const w = canvas.width;
const h = canvas.height;
const step = conf.gridStep;
const pxStep = step * conf.scale;
ctx.clearRect(0, 0, w, h);
// Grid Background
ctx.strokeStyle = '#e0e0e0';
ctx.lineWidth = 1;
ctx.font = '11px Arial';
ctx.fillStyle = '#888';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// Vertical Lines (X)
const minLogicX = Math.floor((0 - conf.origin.x) / conf.scale);
const maxLogicX = Math.ceil((w - conf.origin.x) / conf.scale);
for(let i = minLogicX; i <= maxLogicX; i += step) {
const xPos = conf.origin.x + i * conf.scale;
ctx.beginPath();
ctx.moveTo(xPos, 0);
ctx.lineTo(xPos, h);
ctx.stroke();
if (i !== 0) ctx.fillText(i.toString(), xPos, conf.origin.y + 15);
}
// Horizontal Lines (Y)
const minLogicY = Math.floor((h - conf.origin.y) / (-conf.scale));
const maxLogicY = Math.ceil((0 - conf.origin.y) / (-conf.scale));
const yStart = Math.min(minLogicY, maxLogicY);
const yEnd = Math.max(minLogicY, maxLogicY);
for(let j = yStart; j <= yEnd; j += step) {
const yPos = conf.origin.y - j * conf.scale;
ctx.beginPath();
ctx.moveTo(0, yPos);
ctx.lineTo(w, yPos);
ctx.stroke();
if (j !== 0) ctx.fillText(j.toString(), conf.origin.x - 15, yPos);
}
// Main Axes
ctx.beginPath();
ctx.strokeStyle = '#444';
ctx.lineWidth = 2;
// X Axis
ctx.moveTo(0, conf.origin.y);
ctx.lineTo(w, conf.origin.y);
// Y Axis
ctx.moveTo(conf.origin.x, 0);
ctx.lineTo(conf.origin.x, h);
ctx.stroke();
// Origin Label
ctx.fillText("0", conf.origin.x - 10, conf.origin.y + 15);
}
function drawTrajectory(conf) {
ctx.beginPath();
ctx.strokeStyle = '#aaa'; // Ghost path
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
const samples = 400;
let first = true;
for(let i=0; i<=samples; i++) {
const tLocal = conf.tMin + (i/samples) * (conf.tMax - conf.tMin);
const pt = conf.fn(tLocal);
const scr = toScreen(pt.x, pt.y, conf);
if(first) { ctx.moveTo(scr.x, scr.y); first=false; }
else ctx.lineTo(scr.x, scr.y);
}
ctx.stroke();
ctx.setLineDash([]);
}
function renderFrame() {
const conf = getConf();
drawGrid(conf);
drawTrajectory(conf);
// Position
const pt = conf.fn(state.t);
const scr = toScreen(pt.x, pt.y, conf);
// Position Vector (Arrow)
const origin = conf.origin;
drawArrow(ctx, origin.x, origin.y, scr.x, scr.y, '#1976d2');
// Point Mass
ctx.beginPath();
ctx.fillStyle = '#d32f2f';
ctx.arc(scr.x, scr.y, 6, 0, Math.PI*2);
ctx.fill();
// Label
ctx.fillStyle = '#333';
ctx.font = 'bold 12px sans-serif';
ctx.textAlign = 'left';
ctx.fillText(`P(${pt.x.toFixed(1)}, ${pt.y.toFixed(1)})`, scr.x + 10, scr.y - 10);
// Update UI
mathDisplay.innerText = conf.label;
tValDisplay.innerText = state.t.toFixed(2);
}
function updateSlider() {
const conf = getConf();
// Map t from [tMin, tMax] to slider [0, 1000]
const range = conf.tMax - conf.tMin;
const fraction = (state.t - conf.tMin) / range;
tSlider.value = fraction * 1000;
}
function loop() {
if (!state.isRunning) return;
const conf = getConf();
const dt = 0.01 * state.speed;
state.t += dt;
// Loop time
if (state.t > conf.tMax) {
state.t = conf.tMin;
}
updateSlider();
renderFrame();
state.animFrame = requestAnimationFrame(loop);
}
// --- Events ---
curveSelect.addEventListener('change', (e) => {
state.curveKey = e.target.value;
// Reset t
const conf = getConf();
state.t = conf.tMin;
// Reset UI
updateSlider();
renderFrame();
});
tSlider.addEventListener('input', (e) => {
// Pause on manual interaction
state.isRunning = false;
cancelAnimationFrame(state.animFrame);
btnStart.textContent = "Start";
btnStart.classList.remove('btn-pause');
btnStart.classList.add('btn-start');
const val = parseInt(e.target.value);
const conf = getConf();
const range = conf.tMax - conf.tMin;
state.t = conf.tMin + (val / 1000) * range;
renderFrame();
});
speedSlider.addEventListener('input', (e) => {
state.speed = parseFloat(e.target.value);
});
btnStart.addEventListener('click', () => {
state.isRunning = !state.isRunning;
if(state.isRunning) {
btnStart.textContent = "Pause";
btnStart.classList.remove('btn-start');
btnStart.classList.add('btn-pause');
loop();
} else {
btnStart.textContent = "Start";
btnStart.classList.remove('btn-pause');
btnStart.classList.add('btn-start');
cancelAnimationFrame(state.animFrame);
}
});
btnReset.addEventListener('click', () => {
state.isRunning = false;
cancelAnimationFrame(state.animFrame);
btnStart.textContent = "Start";
btnStart.classList.remove('btn-pause');
btnStart.classList.add('btn-start');
const conf = getConf();
state.t = conf.tMin;
updateSlider();
renderFrame();
});
// Start
renderFrame();
})();
</script>
</div>
```
## Velocity
The velocity of the particle is the derivative of the position vector with respect to time. The velocity vector is defined as
$$
\mathbf{v}(t) = \frac{d\mathbf{r}(t)}{dt} = \left(\frac{dx(t)}{dt}, \frac{dy(t)}{dt}\right)
$$
The velocity vector describes the speed and direction of the particle at any time $t$. The magnitude of the velocity vector is the speed of the particle. The direction of the velocity vector is the direction of motion of the particle.
```{python}
import numpy as np
import matplotlib.pyplot as plt
def r(t):
return np.array([t, -t**2 + t])
def v(t):
return np.array([1, -2*t + 1])
t = np.linspace(0, 10, 100)
fig, axes = plt.subplots(figsize=(8, 6))
# whole curve
axes.plot(r(t)[0], r(t)[1], label='r(t)')
# velocity vector at t=3
axes.quiver(r(3)[0], r(3)[1], v(3)[0], v(3)[1], angles='xy', scale_units='xy', scale=1, color='red', label='v(3)')
axes.text(r(3)[0] + v(3)[0], r(3)[1] + v(3)[1], 'v(3)', color='red')
#velocity vector at t=6
axes.quiver(r(6)[0], r(6)[1], v(6)[0], v(6)[1], angles='xy', scale_units='xy', scale=1, color='blue', label='v(6)')
axes.text(r(6)[0] + v(6)[0], r(6)[1] + v(6)[1], 'v(6)', color='blue')
axes.set_xlabel('x')
axes.set_ylabel('y')
axes.legend()
plt.show()
```
```{=html}
<div class="parametric-motion-widget">
<style>
.parametric-motion-widget {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
padding: 20px;
background-color: #ffffff;
border: 1px solid #e0e0e0;
border-radius: 8px;
color: #333;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
max-width: 800px;
margin: 20px auto;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
box-sizing: border-box;
}
.parametric-motion-widget h3 {
margin-top: 0;
color: #444;
font-size: 1.1em;
text-transform: uppercase;
letter-spacing: 1px;
}
.parametric-motion-widget .canvas-wrapper {
position: relative;
margin: 10px 0 20px 0;
background: #fdfdfd;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: inset 0 0 10px rgba(0,0,0,0.02);
}
.parametric-motion-widget canvas {
display: block;
cursor: crosshair;
}
.parametric-motion-widget .controls-panel {
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
background: #f5f5f5;
padding: 15px;
border-radius: 6px;
border: 1px solid #eee;
}
.parametric-motion-widget .control-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.parametric-motion-widget label {
font-weight: 600;
font-size: 0.85em;
color: #555;
display: flex;
justify-content: space-between;
align-items: center;
}
.parametric-motion-widget select,
.parametric-motion-widget button {
padding: 8px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
background: white;
cursor: pointer;
transition: all 0.2s;
}
.parametric-motion-widget button {
font-weight: bold;
color: white;
border: none;
}
.parametric-motion-widget .btn-start { background-color: #2e7d32; }
.parametric-motion-widget .btn-start:hover { background-color: #1b5e20; }
.parametric-motion-widget .btn-pause { background-color: #f57c00; }
.parametric-motion-widget .btn-reset {
background-color: #607d8b;
margin-top: 5px;
}
.parametric-motion-widget .btn-reset:hover { background-color: #455a64; }
.parametric-motion-widget input[type=range] {
width: 100%;
cursor: pointer;
}
.parametric-motion-widget input[type=checkbox] {
width: auto;
cursor: pointer;
}
.parametric-motion-widget .math-info {
grid-column: 1 / -1;
background: white;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
text-align: center;
font-family: 'Courier New', monospace;
font-weight: bold;
color: #1565c0;
font-size: 1.1em;
}
.parametric-motion-widget .value-display {
color: #1565c0;
}
.parametric-motion-widget .legend {
grid-column: 1 / -1;
display: flex;
justify-content: center;
gap: 15px;
font-size: 0.85em;
margin-top: 5px;
}
.parametric-motion-widget .legend-item {
display: flex;
align-items: center;
gap: 5px;
}
.parametric-motion-widget .color-box {
width: 12px;
height: 12px;
border-radius: 2px;
}
/* Responsiveness */
@media (max-width: 600px) {
.parametric-motion-widget .controls-panel {
grid-template-columns: 1fr;
}
}
</style>
<h3>Visualization of Position r(t) and Velocity v(t)</h3>
<div class="canvas-wrapper">
<canvas class="param-canvas" width="700" height="450"></canvas>
</div>
<div class="controls-panel">
<div class="math-info">r(t) = ...</div>
<div class="control-group">
<label>Select Trajectory:</label>
<select class="curve-select">
<option value="parabola">Parabola (Projectile)</option>
<option value="circle">Circle</option>
<option value="lissajous">Lissajous Curve</option>
<option value="spiral">Archimedean Spiral</option>
</select>
<label style="margin-top: 5px; cursor: pointer;">
<span>Show Velocity Vector</span>
<input type="checkbox" class="chk-velocity" checked>
</label>
<button class="btn-start">Start</button>
<button class="btn-reset">Reset</button>
</div>
<div class="control-group">
<label>Time t: <span class="t-val value-display">0.00</span></label>
<input type="range" class="t-slider" min="0" max="1000" step="1" value="0">
<label style="margin-top: 10px;">Animation Speed:</label>
<input type="range" class="speed-slider" min=".5" max="3" step=".5" value="3">
</div>
<div class="legend">
<div class="legend-item">
<div class="color-box" style="background: #1976d2;"></div> Position r(t)
</div>
<div class="legend-item">
<div class="color-box" style="background: #2e7d32;"></div> Velocity v(t)
</div>
<div class="legend-item">
<div class="color-box" style="background: #d32f2f; border-radius: 50%;"></div> Particle
</div>
</div>
</div>
<script>
(function() {
const widget = document.currentScript.parentElement;
const canvas = widget.querySelector('.param-canvas');
const ctx = canvas.getContext('2d');
// UI Elements
const curveSelect = widget.querySelector('.curve-select');
const btnStart = widget.querySelector('.btn-start');
const btnReset = widget.querySelector('.btn-reset');
const tSlider = widget.querySelector('.t-slider');
const tValDisplay = widget.querySelector('.t-val');
const speedSlider = widget.querySelector('.speed-slider');
const mathDisplay = widget.querySelector('.math-info');
const chkVelocity = widget.querySelector('.chk-velocity');
let state = {
t: 0,
curveKey: 'parabola',
isRunning: false,
speed: 3,
showVelocity: true,
animFrame: null
};
// Curve Configuration
// fn: r(t) -> position {x, y}
// d_fn: v(t) = r'(t) -> velocity {x, y}
// velScale: visual scaling factor for velocity vector length
const curves = {
parabola: {
label: "r(t) = [ 2t, -t² + 4t ]",
fn: (t) => ({ x: 2*t, y: -t*t + 4*t }),
d_fn: (t) => ({ x: 2, y: -2*t + 4 }),
tMin: 0, tMax: 4.5,
scale: 60,
velScale: 0.5,
origin: { x: 50, y: 380 },
gridStep: 1
},
circle: {
label: "r(t) = [ 3cos(t), 3sin(t) ]",
fn: (t) => ({ x: 3*Math.cos(t), y: 3*Math.sin(t) }),
d_fn: (t) => ({ x: -3*Math.sin(t), y: 3*Math.cos(t) }),
tMin: 0, tMax: 2 * Math.PI,
scale: 50,
velScale: 0.5,
origin: { x: 350, y: 225 },
gridStep: 1
},
lissajous: {
label: "r(t) = [ 4sin(3t), 3sin(2t) ]",
fn: (t) => ({ x: 4*Math.sin(3*t), y: 3*Math.sin(2*t) }),
d_fn: (t) => ({ x: 12*Math.cos(3*t), y: 6*Math.cos(2*t) }),
tMin: 0, tMax: 2 * Math.PI,
scale: 45,
velScale: 0.2, // Velocity is large here, scale down visual
origin: { x: 350, y: 225 },
gridStep: 1
},
spiral: {
label: "r(t) = [ 0.5t·cos(t), 0.5t·sin(t) ]",
fn: (t) => ({ x: 0.5*t*Math.cos(t), y: 0.5*t*Math.sin(t) }),
d_fn: (t) => ({
x: 0.5*(Math.cos(t) - t*Math.sin(t)),
y: 0.5*(Math.sin(t) + t*Math.cos(t))
}),
tMin: 0, tMax: 4 * Math.PI,
scale: 40,
velScale: 0.5,
origin: { x: 350, y: 225 },
gridStep: 1
}
};
function getConf() { return curves[state.curveKey]; }
// Logic -> Screen Conversion (Y flips: logic up is screen down)
function toScreen(lx, ly, conf) {
return {
x: conf.origin.x + lx * conf.scale,
y: conf.origin.y - ly * conf.scale
};
}
// Draw Arrow (Vector)
function drawArrow(ctx, fromX, fromY, toX, toY, color, width=2.5) {
const headlen = 10;
const dx = toX - fromX;
const dy = toY - fromY;
const len = Math.sqrt(dx*dx + dy*dy);
if(len < 1) return; // Don't draw tiny arrows
const angle = Math.atan2(dy, dx);
ctx.beginPath();
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.lineWidth = width;
// Line
ctx.moveTo(fromX, fromY);
ctx.lineTo(toX, toY);
ctx.stroke();
// Arrow Head
ctx.beginPath();
ctx.moveTo(toX, toY);
ctx.lineTo(toX - headlen * Math.cos(angle - Math.PI / 6), toY - headlen * Math.sin(angle - Math.PI / 6));
ctx.lineTo(toX - headlen * Math.cos(angle + Math.PI / 6), toY - headlen * Math.sin(angle + Math.PI / 6));
ctx.lineTo(toX, toY);
ctx.fill();
}
function drawGrid(conf) {
const w = canvas.width;
const h = canvas.height;
const step = conf.gridStep;
ctx.clearRect(0, 0, w, h);
// Grid Background
ctx.strokeStyle = '#e0e0e0';
ctx.lineWidth = 1;
ctx.font = '11px Arial';
ctx.fillStyle = '#888';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// Vertical Lines (X)
const minLogicX = Math.floor((0 - conf.origin.x) / conf.scale);
const maxLogicX = Math.ceil((w - conf.origin.x) / conf.scale);
for(let i = minLogicX; i <= maxLogicX; i += step) {
const xPos = conf.origin.x + i * conf.scale;
ctx.beginPath();
ctx.moveTo(xPos, 0);
ctx.lineTo(xPos, h);
ctx.stroke();
if (i !== 0) ctx.fillText(i.toString(), xPos, conf.origin.y + 15);
}
// Horizontal Lines (Y)
const minLogicY = Math.floor((h - conf.origin.y) / (-conf.scale));
const maxLogicY = Math.ceil((0 - conf.origin.y) / (-conf.scale));
const yStart = Math.min(minLogicY, maxLogicY);
const yEnd = Math.max(minLogicY, maxLogicY);
for(let j = yStart; j <= yEnd; j += step) {
const yPos = conf.origin.y - j * conf.scale;
ctx.beginPath();
ctx.moveTo(0, yPos);
ctx.lineTo(w, yPos);
ctx.stroke();
if (j !== 0) ctx.fillText(j.toString(), conf.origin.x - 15, yPos);
}
// Main Axes
ctx.beginPath();
ctx.strokeStyle = '#444';
ctx.lineWidth = 2;
// X Axis
ctx.moveTo(0, conf.origin.y);
ctx.lineTo(w, conf.origin.y);
// Y Axis
ctx.moveTo(conf.origin.x, 0);
ctx.lineTo(conf.origin.x, h);
ctx.stroke();
// Origin Label
ctx.fillText("0", conf.origin.x - 10, conf.origin.y + 15);
}
function drawTrajectory(conf) {
ctx.beginPath();
ctx.strokeStyle = '#aaa'; // Ghost path
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
const samples = 400;
let first = true;
for(let i=0; i<=samples; i++) {
const tLocal = conf.tMin + (i/samples) * (conf.tMax - conf.tMin);
const pt = conf.fn(tLocal);
const scr = toScreen(pt.x, pt.y, conf);
if(first) { ctx.moveTo(scr.x, scr.y); first=false; }
else ctx.lineTo(scr.x, scr.y);
}
ctx.stroke();
ctx.setLineDash([]);
}
function renderFrame() {
const conf = getConf();
drawGrid(conf);
drawTrajectory(conf);
// Logic Position
const pt = conf.fn(state.t);
// Screen Position
const scr = toScreen(pt.x, pt.y, conf);
// 1. Position Vector (Blue Arrow) from Origin to Point
const origin = conf.origin;
drawArrow(ctx, origin.x, origin.y, scr.x, scr.y, '#1976d2');
// 2. Velocity Vector (Green Arrow) from Point
if(state.showVelocity) {
const vel = conf.d_fn(state.t);
// Scale velocity for visualization (logic units)
const visVelX = vel.x * conf.velScale;
const visVelY = vel.y * conf.velScale;
// Screen coordinates for velocity end point
// Note: dy is subtracted because screen Y is inverted relative to logic Y
const velEnd = {
x: scr.x + visVelX * conf.scale,
y: scr.y - visVelY * conf.scale
};
drawArrow(ctx, scr.x, scr.y, velEnd.x, velEnd.y, '#2e7d32', 3);
}
// Point Mass (Red Dot)
ctx.beginPath();
ctx.fillStyle = '#d32f2f';
ctx.arc(scr.x, scr.y, 6, 0, Math.PI*2);
ctx.fill();
// Label P(x, y)
ctx.fillStyle = '#333';
ctx.font = 'bold 12px sans-serif';
ctx.textAlign = 'left';
ctx.fillText(`P`, scr.x + 10, scr.y - 10);
// Update UI
mathDisplay.innerText = conf.label;
tValDisplay.innerText = state.t.toFixed(2);
}
function updateSlider() {
const conf = getConf();
// Map t from [tMin, tMax] to slider [0, 1000]
const range = conf.tMax - conf.tMin;
const fraction = (state.t - conf.tMin) / range;
tSlider.value = fraction * 1000;
}
function loop() {
if (!state.isRunning) return;
const conf = getConf();
const dt = 0.01 * state.speed;
state.t += dt;
// Loop time
if (state.t > conf.tMax) {
state.t = conf.tMin;
}
updateSlider();
renderFrame();
state.animFrame = requestAnimationFrame(loop);
}
// --- Events ---
curveSelect.addEventListener('change', (e) => {
state.curveKey = e.target.value;
// Reset t
const conf = getConf();
state.t = conf.tMin;
// Reset UI
updateSlider();
renderFrame();
});
chkVelocity.addEventListener('change', (e) => {
state.showVelocity = e.target.checked;
renderFrame();
});
tSlider.addEventListener('input', (e) => {
// Pause on manual interaction
state.isRunning = false;
cancelAnimationFrame(state.animFrame);
btnStart.textContent = "Start";
btnStart.classList.remove('btn-pause');
btnStart.classList.add('btn-start');
const val = parseInt(e.target.value);
const conf = getConf();
const range = conf.tMax - conf.tMin;
state.t = conf.tMin + (val / 1000) * range;
renderFrame();
});
speedSlider.addEventListener('input', (e) => {
state.speed = parseFloat(e.target.value);
});
btnStart.addEventListener('click', () => {
state.isRunning = !state.isRunning;
if(state.isRunning) {
btnStart.textContent = "Pause";
btnStart.classList.remove('btn-start');
btnStart.classList.add('btn-pause');
loop();
} else {
btnStart.textContent = "Start";
btnStart.classList.remove('btn-pause');
btnStart.classList.add('btn-start');
cancelAnimationFrame(state.animFrame);
}
});
btnReset.addEventListener('click', () => {
state.isRunning = false;
cancelAnimationFrame(state.animFrame);
btnStart.textContent = "Start";
btnStart.classList.remove('btn-pause');
btnStart.classList.add('btn-start');
const conf = getConf();
state.t = conf.tMin;
updateSlider();
renderFrame();
});
// Start
renderFrame();
})();
</script>
</div>
```
## Acceleration
The acceleration of the particle is the derivative of the velocity vector with respect to time. The acceleration vector is defined as
$$
\mathbf{a}(t) =
\frac{d\mathbf{v}(t)}{dt} =
\left(\frac{dv_x(t)}{dt}, \frac{dv_y(t)}{dt}\right) =
\left(\frac{d^2x(t)}{dt^2}, \frac{d^2y(t)}{dt^2}\right)
$$
The acceleration vector describes the rate of change of the velocity of the particle at any time $t$. The magnitude of the acceleration vector is the rate of change of the speed of the particle. The direction of the acceleration vector is the direction of the change of the velocity of the particle.
```{python}
import numpy as np
import matplotlib.pyplot as plt
def r(t):
return np.array([t, -t**2 + t])
def v(t):
return np.array([1, -2*t + 1])
def a(t):
return np.array([0, -2])
t = np.linspace(0, 4, 100)
fig, axes = plt.subplots(figsize=(8, 6))
# whole curve
axes.plot(r(t)[0], r(t)[1], label='r(t)')
# velocity vector at t=3
axes.quiver(r(3)[0], r(3)[1], v(3)[0], v(3)[1], angles='xy', scale_units='xy', scale=1, color='red', label='v(3)')
axes.text(r(3)[0] + v(3)[0], r(3)[1] + v(3)[1], 'v(3)', color='red')
# acceleration vector at t=3
axes.quiver(r(3)[0], r(3)[1], a(3)[0], a(3)[1], angles='xy', scale_units='xy', scale=1, color='blue', label='a(3)')
axes.text(r(3)[0] + a(3)[0], r(3)[1] + a(3)[1], 'a(3)', color='blue')
axes.set_xlabel('x')
axes.set_ylabel('y')
axes.legend()
plt.show()
```
```{=html}
<div class="parametric-motion-widget">
<style>
.parametric-motion-widget {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
padding: 20px;
background-color: #ffffff;
border: 1px solid #e0e0e0;
border-radius: 8px;
color: #333;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
max-width: 800px;
margin: 20px auto;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
box-sizing: border-box;
}
.parametric-motion-widget h3 {
margin-top: 0;
color: #444;
font-size: 1.1em;
text-transform: uppercase;
letter-spacing: 1px;
text-align: center;
}
.parametric-motion-widget .canvas-wrapper {
position: relative;
margin: 10px 0 20px 0;
background: #fdfdfd;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: inset 0 0 10px rgba(0,0,0,0.02);
}
.parametric-motion-widget canvas {
display: block;
cursor: crosshair;
}
.parametric-motion-widget .controls-panel {
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
background: #f5f5f5;
padding: 15px;
border-radius: 6px;
border: 1px solid #eee;
}
.parametric-motion-widget .control-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.parametric-motion-widget label {
font-weight: 600;
font-size: 0.85em;
color: #555;
display: flex;
justify-content: space-between;
align-items: center;
}
.parametric-motion-widget select,
.parametric-motion-widget button {
padding: 8px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
background: white;
cursor: pointer;
transition: all 0.2s;
}
.parametric-motion-widget button {
font-weight: bold;
color: white;
border: none;
}
.parametric-motion-widget .btn-start { background-color: #2e7d32; }
.parametric-motion-widget .btn-start:hover { background-color: #1b5e20; }
.parametric-motion-widget .btn-pause { background-color: #f57c00; }
.parametric-motion-widget .btn-reset {
background-color: #607d8b;
margin-top: 5px;
}
.parametric-motion-widget .btn-reset:hover { background-color: #455a64; }
.parametric-motion-widget input[type=range] {
width: 100%;
cursor: pointer;
}
.parametric-motion-widget input[type=checkbox] {
width: auto;
cursor: pointer;
}
.parametric-motion-widget .math-info {
grid-column: 1 / -1;
background: white;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
text-align: center;
font-family: 'Courier New', monospace;
font-weight: bold;
color: #1565c0;
font-size: 1.0em;
display: flex;
flex-direction: column;
gap: 5px;
}
.parametric-motion-widget .value-display {
color: #1565c0;
}
.parametric-motion-widget .legend {
grid-column: 1 / -1;
display: flex;
justify-content: center;
gap: 15px;
font-size: 0.85em;
margin-top: 5px;
flex-wrap: wrap;
}
.parametric-motion-widget .legend-item {
display: flex;
align-items: center;
gap: 5px;
}
.parametric-motion-widget .color-box {
width: 12px;
height: 12px;
border-radius: 2px;
}
/* Responsiveness */
@media (max-width: 600px) {
.parametric-motion-widget .controls-panel {
grid-template-columns: 1fr;
}
}
</style>
<h3>Kinematics: Position r, Velocity v, Acceleration a</h3>
<div class="canvas-wrapper">
<canvas class="param-canvas" width="700" height="450"></canvas>
</div>
<div class="controls-panel">
<div class="math-info">
<!-- Content filled by JS -->
</div>
<div class="control-group">
<label>Select Trajectory:</label>
<select class="curve-select">
<option value="parabola">Parabola (Projectile)</option>
<option value="circle">Circle</option>
<option value="lissajous">Lissajous Curve</option>
<option value="spiral">Archimedean Spiral</option>
</select>
<label style="margin-top: 5px; cursor: pointer;">
<span>Show Velocity Vector</span>
<input type="checkbox" class="chk-velocity" checked>
</label>
<label style="margin-top: 5px; cursor: pointer;">
<span>Show Acceleration Vector</span>
<input type="checkbox" class="chk-acceleration" checked>
</label>
<button class="btn-start">Start</button>
<button class="btn-reset">Reset</button>
</div>
<div class="control-group">
<label>Time t: <span class="t-val value-display">0.00</span></label>
<input type="range" class="t-slider" min="0" max="1000" step="1" value="0">
<label style="margin-top: 10px;">Animation Speed:</label>
<input type="range" class="speed-slider" min="1" max="10" step="1" value="3">
</div>
<div class="legend">
<div class="legend-item">
<div class="color-box" style="background: #1976d2;"></div> Position r(t)
</div>
<div class="legend-item">
<div class="color-box" style="background: #2e7d32;"></div> Velocity v(t)
</div>
<div class="legend-item">
<div class="color-box" style="background: #f57f17;"></div> Acceleration a(t)
</div>
<div class="legend-item">
<div class="color-box" style="background: #d32f2f; border-radius: 50%;"></div> Particle
</div>
</div>
</div>
<script>
(function() {
const widget = document.currentScript.parentElement;
const canvas = widget.querySelector('.param-canvas');
const ctx = canvas.getContext('2d');
// UI Elements
const curveSelect = widget.querySelector('.curve-select');
const btnStart = widget.querySelector('.btn-start');
const btnReset = widget.querySelector('.btn-reset');
const tSlider = widget.querySelector('.t-slider');
const tValDisplay = widget.querySelector('.t-val');
const speedSlider = widget.querySelector('.speed-slider');
const mathDisplay = widget.querySelector('.math-info');
const chkVelocity = widget.querySelector('.chk-velocity');
const chkAcceleration = widget.querySelector('.chk-acceleration');
let state = {
t: 0,
curveKey: 'parabola',
isRunning: false,
speed: 3,
showVelocity: true,
showAcceleration: true,
animFrame: null
};
// Curve Configuration
// fn: r(t) -> position {x, y}
// d_fn: v(t) = r'(t) -> velocity {x, y}
// dd_fn: a(t) = r''(t) -> acceleration {x, y}
// velScale: visual scaling factor for velocity
// accScale: visual scaling factor for acceleration
const curves = {
parabola: {
label: "r(t) = [ 2t, -t² + 4t ]",
label_v: "v(t) = [ 2, -2t + 4 ]",
label_a: "a(t) = [ 0, -2 ]",
fn: (t) => ({ x: 2*t, y: -t*t + 4*t }),
d_fn: (t) => ({ x: 2, y: -2*t + 4 }),
dd_fn: (t) => ({ x: 0, y: -2 }),
tMin: 0, tMax: 4.5,
scale: 60,
velScale: 0.5,
accScale: 1.5,
origin: { x: 50, y: 380 },
gridStep: 1
},
circle: {
label: "r(t) = [ 3cos(t), 3sin(t) ]",
label_v: "v(t) = [ -3sin(t), 3cos(t) ]",
label_a: "a(t) = [ -3cos(t), -3sin(t) ]",
fn: (t) => ({ x: 3*Math.cos(t), y: 3*Math.sin(t) }),
d_fn: (t) => ({ x: -3*Math.sin(t), y: 3*Math.cos(t) }),
dd_fn: (t) => ({ x: -3*Math.cos(t), y: -3*Math.sin(t) }),
tMin: 0, tMax: 2 * Math.PI,
scale: 50,
velScale: 0.5,
accScale: 0.5,
origin: { x: 350, y: 225 },
gridStep: 1
},
lissajous: {
label: "r(t) = [ 4sin(3t), 3sin(2t) ]",
label_v: "v(t) = [ 12cos(3t), 6cos(2t) ]",
label_a: "a(t) = [ -36sin(3t), -12sin(2t) ]",
fn: (t) => ({ x: 4*Math.sin(3*t), y: 3*Math.sin(2*t) }),
d_fn: (t) => ({ x: 12*Math.cos(3*t), y: 6*Math.cos(2*t) }),
dd_fn: (t) => ({ x: -36*Math.sin(3*t), y: -12*Math.sin(2*t) }),
tMin: 0, tMax: 2 * Math.PI,
scale: 45,
velScale: 0.2,
accScale: 0.05,
origin: { x: 350, y: 225 },
gridStep: 1
},
spiral: {
label: "r(t) = [ 0.5t·cos(t), 0.5t·sin(t) ]",
label_v: "v(t) = [ 0.5(cos(t)-t·sin(t)), 0.5(sin(t)+t·cos(t)) ]",
label_a: "a(t) = [ 0.5(-2sin(t)-t·cos(t)), 0.5(2cos(t)-t·sin(t)) ]",
fn: (t) => ({ x: 0.5*t*Math.cos(t), y: 0.5*t*Math.sin(t) }),
d_fn: (t) => ({
x: 0.5*(Math.cos(t) - t*Math.sin(t)),
y: 0.5*(Math.sin(t) + t*Math.cos(t))
}),
dd_fn: (t) => ({
x: 0.5*(-2*Math.sin(t) - t*Math.cos(t)),
y: 0.5*(2*Math.cos(t) - t*Math.sin(t))
}),
tMin: 0, tMax: 4 * Math.PI,
scale: 40,
velScale: 0.5,
accScale: 0.2,
origin: { x: 350, y: 225 },
gridStep: 1
}
};
function getConf() { return curves[state.curveKey]; }
// Logic -> Screen Conversion (Y flips: logic up is screen down)
function toScreen(lx, ly, conf) {
return {
x: conf.origin.x + lx * conf.scale,
y: conf.origin.y - ly * conf.scale
};
}
// Draw Arrow (Vector)
function drawArrow(ctx, fromX, fromY, toX, toY, color, width=2.5) {
const headlen = 10;
const dx = toX - fromX;
const dy = toY - fromY;
const len = Math.sqrt(dx*dx + dy*dy);
if(len < 1) return; // Don't draw tiny arrows
const angle = Math.atan2(dy, dx);
ctx.beginPath();
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.lineWidth = width;
// Line
ctx.moveTo(fromX, fromY);
ctx.lineTo(toX, toY);
ctx.stroke();
// Arrow Head
ctx.beginPath();
ctx.moveTo(toX, toY);
ctx.lineTo(toX - headlen * Math.cos(angle - Math.PI / 6), toY - headlen * Math.sin(angle - Math.PI / 6));
ctx.lineTo(toX - headlen * Math.cos(angle + Math.PI / 6), toY - headlen * Math.sin(angle + Math.PI / 6));
ctx.lineTo(toX, toY);
ctx.fill();
}
function drawGrid(conf) {
const w = canvas.width;
const h = canvas.height;
const step = conf.gridStep;
ctx.clearRect(0, 0, w, h);
// Grid Background
ctx.strokeStyle = '#e0e0e0';
ctx.lineWidth = 1;
ctx.font = '11px Arial';
ctx.fillStyle = '#888';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// Vertical Lines (X)
const minLogicX = Math.floor((0 - conf.origin.x) / conf.scale);
const maxLogicX = Math.ceil((w - conf.origin.x) / conf.scale);
for(let i = minLogicX; i <= maxLogicX; i += step) {
const xPos = conf.origin.x + i * conf.scale;
ctx.beginPath();
ctx.moveTo(xPos, 0);
ctx.lineTo(xPos, h);
ctx.stroke();
if (i !== 0) ctx.fillText(i.toString(), xPos, conf.origin.y + 15);
}
// Horizontal Lines (Y)
const minLogicY = Math.floor((h - conf.origin.y) / (-conf.scale));
const maxLogicY = Math.ceil((0 - conf.origin.y) / (-conf.scale));
const yStart = Math.min(minLogicY, maxLogicY);
const yEnd = Math.max(minLogicY, maxLogicY);
for(let j = yStart; j <= yEnd; j += step) {
const yPos = conf.origin.y - j * conf.scale;
ctx.beginPath();
ctx.moveTo(0, yPos);
ctx.lineTo(w, yPos);
ctx.stroke();
if (j !== 0) ctx.fillText(j.toString(), conf.origin.x - 15, yPos);
}
// Main Axes
ctx.beginPath();
ctx.strokeStyle = '#444';
ctx.lineWidth = 2;
// X Axis
ctx.moveTo(0, conf.origin.y);
ctx.lineTo(w, conf.origin.y);
// Y Axis
ctx.moveTo(conf.origin.x, 0);
ctx.lineTo(conf.origin.x, h);
ctx.stroke();
// Origin Label
ctx.fillText("0", conf.origin.x - 10, conf.origin.y + 15);
}
function drawTrajectory(conf) {
ctx.beginPath();
ctx.strokeStyle = '#aaa'; // Ghost path
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
const samples = 400;
let first = true;
for(let i=0; i<=samples; i++) {
const tLocal = conf.tMin + (i/samples) * (conf.tMax - conf.tMin);
const pt = conf.fn(tLocal);
const scr = toScreen(pt.x, pt.y, conf);
if(first) { ctx.moveTo(scr.x, scr.y); first=false; }
else ctx.lineTo(scr.x, scr.y);
}
ctx.stroke();
ctx.setLineDash([]);
}
function renderFrame() {
const conf = getConf();
drawGrid(conf);
drawTrajectory(conf);
// Logic Position
const pt = conf.fn(state.t);
// Screen Position
const scr = toScreen(pt.x, pt.y, conf);
// 1. Position Vector (Blue Arrow) from Origin to Point
const origin = conf.origin;
drawArrow(ctx, origin.x, origin.y, scr.x, scr.y, '#1976d2');
// 2. Velocity Vector (Green Arrow) from Point
if(state.showVelocity) {
const vel = conf.d_fn(state.t);
const visVelX = vel.x * conf.velScale;
const visVelY = vel.y * conf.velScale;
const velEnd = {
x: scr.x + visVelX * conf.scale,
y: scr.y - visVelY * conf.scale
};
drawArrow(ctx, scr.x, scr.y, velEnd.x, velEnd.y, '#2e7d32', 3);
}
// 3. Acceleration Vector (Orange Arrow) from Point
if(state.showAcceleration) {
const acc = conf.dd_fn(state.t);
const visAccX = acc.x * conf.accScale;
const visAccY = acc.y * conf.accScale;
const accEnd = {
x: scr.x + visAccX * conf.scale,
y: scr.y - visAccY * conf.scale
};
drawArrow(ctx, scr.x, scr.y, accEnd.x, accEnd.y, '#f57f17', 3);
}
// Point Mass (Red Dot)
ctx.beginPath();
ctx.fillStyle = '#d32f2f';
ctx.arc(scr.x, scr.y, 6, 0, Math.PI*2);
ctx.fill();
// Label P
ctx.fillStyle = '#333';
ctx.font = 'bold 12px sans-serif';
ctx.textAlign = 'left';
ctx.fillText(`P`, scr.x + 10, scr.y - 10);
// Update UI - Math Labels with colors matching vectors
mathDisplay.innerHTML = `
<div style="color: #1565c0;">${conf.label}</div>
<div style="color: #2e7d32;">${conf.label_v}</div>
<div style="color: #f57f17;">${conf.label_a}</div>
`;
tValDisplay.innerText = state.t.toFixed(2);
}
function updateSlider() {
const conf = getConf();
// Map t from [tMin, tMax] to slider [0, 1000]
const range = conf.tMax - conf.tMin;
const fraction = (state.t - conf.tMin) / range;
tSlider.value = fraction * 1000;
}
function loop() {
if (!state.isRunning) return;
const conf = getConf();
const dt = 0.01 * state.speed;
state.t += dt;
// Loop time
if (state.t > conf.tMax) {
state.t = conf.tMin;
}
updateSlider();
renderFrame();
state.animFrame = requestAnimationFrame(loop);
}
// --- Events ---
curveSelect.addEventListener('change', (e) => {
state.curveKey = e.target.value;
// Reset t
const conf = getConf();
state.t = conf.tMin;
// Reset UI
updateSlider();
renderFrame();
});
chkVelocity.addEventListener('change', (e) => {
state.showVelocity = e.target.checked;
renderFrame();
});
chkAcceleration.addEventListener('change', (e) => {
state.showAcceleration = e.target.checked;
renderFrame();
});
tSlider.addEventListener('input', (e) => {
// Pause on manual interaction
state.isRunning = false;
cancelAnimationFrame(state.animFrame);
btnStart.textContent = "Start";
btnStart.classList.remove('btn-pause');
btnStart.classList.add('btn-start');
const val = parseInt(e.target.value);
const conf = getConf();
const range = conf.tMax - conf.tMin;
state.t = conf.tMin + (val / 1000) * range;
renderFrame();
});
speedSlider.addEventListener('input', (e) => {
state.speed = parseFloat(e.target.value);
});
btnStart.addEventListener('click', () => {
state.isRunning = !state.isRunning;
if(state.isRunning) {
btnStart.textContent = "Pause";
btnStart.classList.remove('btn-start');
btnStart.classList.add('btn-pause');
loop();
} else {
btnStart.textContent = "Start";
btnStart.classList.remove('btn-pause');
btnStart.classList.add('btn-start');
cancelAnimationFrame(state.animFrame);
}
});
btnReset.addEventListener('click', () => {
state.isRunning = false;
cancelAnimationFrame(state.animFrame);
btnStart.textContent = "Start";
btnStart.classList.remove('btn-pause');
btnStart.classList.add('btn-start');
const conf = getConf();
state.t = conf.tMin;
updateSlider();
renderFrame();
});
// Start
renderFrame();
})();
</script>
</div>
```
### Numerical approximation of velocity and acceleration
```{=html}
<div class="numerical-approx-widget">
<style>
.numerical-approx-widget {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
padding: 20px;
background-color: #ffffff;
border: 1px solid #e0e0e0;
border-radius: 8px;
color: #333;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
max-width: 850px;
margin: 20px auto;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
box-sizing: border-box;
}
.numerical-approx-widget h3 {
margin-top: 0;
color: #444;
font-size: 1.2em;
text-transform: uppercase;
letter-spacing: 1px;
text-align: center;
}
.numerical-approx-widget .canvas-wrapper {
position: relative;
margin: 10px 0 20px 0;
background: #fdfdfd;
border: 1px solid #ccc;
border-radius: 4px;
overflow: hidden;
}
.numerical-approx-widget canvas {
display: block;
cursor: crosshair;
}
.numerical-approx-widget .controls-panel {
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
background: #f5f5f5;
padding: 15px;
border-radius: 6px;
border: 1px solid #eee;
}
.numerical-approx-widget .control-group {
display: flex;
flex-direction: column;
gap: 12px;
}
.numerical-approx-widget label {
font-weight: 600;
font-size: 0.85em;
color: #555;
display: flex;
justify-content: space-between;
align-items: center;
}
.numerical-approx-widget select {
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
cursor: pointer;
}
.numerical-approx-widget input[type=range] {
width: 100%;
cursor: pointer;
}
.numerical-approx-widget .checkbox-row {
display: flex;
align-items: center;
gap: 10px;
font-size: 0.9em;
font-weight: 600;
color: #444;
cursor: pointer;
user-select: none;
}
.numerical-approx-widget input[type=checkbox] {
width: 18px;
height: 18px;
cursor: pointer;
}
.numerical-approx-widget .legend {
grid-column: 1 / -1;
display: flex;
justify-content: center;
gap: 20px;
font-size: 0.85em;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #ddd;
flex-wrap: wrap;
}
.numerical-approx-widget .legend-item {
display: flex;
align-items: center;
gap: 6px;
}
.numerical-approx-widget .color-line {
width: 20px;
height: 3px;
border-radius: 2px;
}
.numerical-approx-widget .color-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
/* Responsiveness */
@media (max-width: 600px) {
.numerical-approx-widget .controls-panel {
grid-template-columns: 1fr;
}
}
</style>
<h3>Numerical Differentiation & Approximation</h3>
<div class="canvas-wrapper">
<canvas class="approx-canvas" width="800" height="450"></canvas>
</div>
<div class="controls-panel">
<div class="control-group">
<label>Select Function / Curve:</label>
<select class="curve-select">
<option value="parabola">Parabola (y = x²)</option>
<option value="sine">Sine Wave (y = sin x)</option>
<option value="circle">Circle</option>
<option value="spiral">Archimedean Spiral</option>
</select>
<label title="Number of intervals (N)">
Resolution (N points): <span class="val-N" style="color:#1976d2">30</span>
</label>
<input type="range" class="slider-N" min="5" max="200" step="1" value="30">
</div>
<div class="control-group">
<label title="Show every K-th vector to reduce clutter">
Vector Visibility (Show every): <span class="val-density" style="color:#555">1</span>
</label>
<input type="range" class="slider-density" min="1" max="20" step="1" value="1">
<div style="margin-top: 10px;"><strong>Display Options:</strong></div>
<label class="checkbox-row">
<input type="checkbox" class="chk-points">
<span>Show Discrete Points (P<sub>i</sub>)</span>
</label>
<label class="checkbox-row">
<input type="checkbox" class="chk-vel" checked>
<span style="color: #2e7d32;">Show 1st Derivative (Gradient)</span>
</label>
<label class="checkbox-row">
<input type="checkbox" class="chk-acc" checked>
<span style="color: #ef6c00;">Show 2nd Derivative (Curvature)</span>
</label>
</div>
<div class="legend">
<div class="legend-item"><div class="color-line" style="background: #ccc; border: 1px dashed #999;"></div> Exact Curve</div>
<div class="legend-item"><div class="color-line" style="background: #666; height: 2px;"></div> Approximation</div>
<div class="legend-item"><div class="color-line" style="background: #2e7d32;"></div> 1st Diff (Δr)</div>
<div class="legend-item"><div class="color-line" style="background: #ef6c00;"></div> 2nd Diff (Δv)</div>
</div>
</div>
<script>
(function() {
const widget = document.currentScript.parentElement;
const canvas = widget.querySelector('.approx-canvas');
const ctx = canvas.getContext('2d');
// UI References
const curveSelect = widget.querySelector('.curve-select');
const sliderN = widget.querySelector('.slider-N');
const sliderDensity = widget.querySelector('.slider-density');
const valN = widget.querySelector('.val-N');
const valDensity = widget.querySelector('.val-density');
const chkVel = widget.querySelector('.chk-vel');
const chkAcc = widget.querySelector('.chk-acc');
const chkPoints = widget.querySelector('.chk-points');
// State
let state = {
curveKey: 'parabola',
N: 30,
densityStep: 1,
showVel: true,
showAcc: true,
showPoints: false
};
// Configuration for Curves
const curves = {
parabola: {
fn: (t) => ({ x: t, y: 0.3*t*t - t }),
tMin: -6, tMax: 6,
baseScale: 50,
origin: { x: 400, y: 350 },
velScale: 0.5,
accScale: 2.0
},
sine: {
fn: (t) => ({ x: t, y: 2*Math.sin(t) }),
tMin: -6, tMax: 6,
baseScale: 60,
origin: { x: 400, y: 225 },
velScale: 0.5,
accScale: 0.5
},
circle: {
fn: (t) => ({ x: 3*Math.cos(t), y: 3*Math.sin(t) }),
tMin: 0, tMax: 2*Math.PI,
baseScale: 60,
origin: { x: 400, y: 225 },
velScale: 0.4,
accScale: 0.4
},
spiral: {
fn: (t) => ({ x: 0.4*t*Math.cos(t), y: 0.4*t*Math.sin(t) }),
tMin: 0, tMax: 6*Math.PI,
baseScale: 40,
origin: { x: 400, y: 225 },
velScale: 0.5,
accScale: 0.5
}
};
// --- Core Math ---
function calculateData() {
const conf = curves[state.curveKey];
const dt = (conf.tMax - conf.tMin) / (state.N - 1);
// 1. Generate Discrete Points
let points = [];
for (let i = 0; i < state.N; i++) {
const t = conf.tMin + i * dt;
const p = conf.fn(t);
points.push(p);
}
// 2. Approximate 1st Derivative (Difference Quotient)
let vels = [];
for (let i = 0; i < state.N - 1; i++) {
const dx = points[i+1].x - points[i].x;
const dy = points[i+1].y - points[i].y;
vels.push({ x: dx/dt, y: dy/dt });
}
if (vels.length > 0) vels.push(vels[vels.length - 1]);
// 3. Approximate 2nd Derivative
let accs = [];
for (let i = 0; i < state.N - 2; i++) {
const dvx = vels[i+1].x - vels[i].x;
const dvy = vels[i+1].y - vels[i].y;
accs.push({ x: dvx/dt, y: dvy/dt });
}
if (accs.length > 0) {
accs.push(accs[accs.length - 1]);
accs.push(accs[accs.length - 1]);
}
return { points, vels, accs, conf };
}
// --- Drawing ---
function toScreen(pt, conf) {
return {
x: conf.origin.x + pt.x * conf.baseScale,
y: conf.origin.y - pt.y * conf.baseScale // Flip Y
};
}
function drawArrow(x, y, vx, vy, color, scale) {
const dx = vx * scale;
const dy = -vy * scale; // Flip Y
const endX = x + dx;
const endY = y + dy;
const len = Math.sqrt(dx*dx + dy*dy);
if(len < 3) return; // Ignore tiny arrows
const angle = Math.atan2(dy, dx);
const headLen = 8;
ctx.beginPath();
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.lineWidth = 2;
ctx.moveTo(x, y);
ctx.lineTo(endX, endY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(endX, endY);
ctx.lineTo(endX - headLen * Math.cos(angle - Math.PI/6), endY - headLen * Math.sin(angle - Math.PI/6));
ctx.lineTo(endX - headLen * Math.cos(angle + Math.PI/6), endY - headLen * Math.sin(angle + Math.PI/6));
ctx.lineTo(endX, endY);
ctx.fill();
}
function drawExactCurve(conf) {
ctx.beginPath();
ctx.strokeStyle = '#ccc';
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
const steps = 400;
let first = true;
for(let i=0; i<=steps; i++) {
const t = conf.tMin + (i/steps) * (conf.tMax - conf.tMin);
const p = conf.fn(t);
const s = toScreen(p, conf);
if(first) { ctx.moveTo(s.x, s.y); first = false; }
else ctx.lineTo(s.x, s.y);
}
ctx.stroke();
ctx.setLineDash([]);
}
function draw() {
const w = canvas.width;
const h = canvas.height;
ctx.clearRect(0, 0, w, h);
const data = calculateData();
const { points, vels, accs, conf } = data;
// 1. Grid
ctx.strokeStyle = '#f5f5f5';
ctx.lineWidth = 1;
ctx.beginPath();
const center = conf.origin;
ctx.moveTo(center.x, 0); ctx.lineTo(center.x, h);
ctx.moveTo(0, center.y); ctx.lineTo(w, center.y);
ctx.stroke();
// 2. Exact Curve
drawExactCurve(conf);
// 3. Approximation
ctx.beginPath();
ctx.strokeStyle = '#666';
ctx.lineWidth = 2;
for(let i=0; i<points.length; i++) {
const s = toScreen(points[i], conf);
if(i===0) ctx.moveTo(s.x, s.y);
else ctx.lineTo(s.x, s.y);
}
ctx.stroke();
// 4. Vectors & Points
for(let i=0; i<points.length; i++) {
const s = toScreen(points[i], conf);
if(state.showPoints) {
ctx.beginPath();
ctx.fillStyle = '#333';
ctx.arc(s.x, s.y, 4, 0, Math.PI*2);
ctx.fill();
}
if (i % state.densityStep !== 0 && i !== points.length-1) continue;
if(state.showVel && vels[i]) {
// Removed extra factor, using baseScale * velScale
drawArrow(s.x, s.y, vels[i].x, vels[i].y, '#2e7d32', conf.baseScale * conf.velScale);
}
if(state.showAcc && accs[i]) {
drawArrow(s.x, s.y, accs[i].x, accs[i].y, '#ef6c00', conf.baseScale * conf.accScale);
}
}
}
// Listeners
curveSelect.addEventListener('change', (e) => {
state.curveKey = e.target.value;
draw();
});
sliderN.addEventListener('input', (e) => {
state.N = parseInt(e.target.value);
valN.textContent = state.N;
draw();
});
sliderDensity.addEventListener('input', (e) => {
state.densityStep = parseInt(e.target.value);
valDensity.textContent = state.densityStep;
draw();
});
chkVel.addEventListener('change', (e) => {
state.showVel = e.target.checked;
draw();
});
chkAcc.addEventListener('change', (e) => {
state.showAcc = e.target.checked;
draw();
});
chkPoints.addEventListener('change', (e) => {
state.showPoints = e.target.checked;
draw();
});
draw();
})();
</script>
</div>
```
## Newton's laws of motion
Revolutions in physics started with Newton's laws of motion. These laws describe the motion of particles in space. The first law states that a particle moves with constant velocity if no external forces act on it. The second law states that the acceleration of a particle is proportional to the force acting on it. The third law states that forces always occur in pairs. If one object exerts a force on another object, the second object exerts an equal and opposite force on the first object. These laws are the foundation of classical mechanics.
### Newton's first law
Newton's first law states that a particle moves with constant velocity if no external forces act on it. This law is also known as the law of inertia. The law of inertia states that an object at rest stays at rest and an object in motion stays in motion with the same speed and in the same direction unless acted upon by an external force. This law is a consequence of the conservation of momentum. The momentum of a particle is the product of its mass and velocity. The momentum of a particle is conserved if no external forces act on it.
### Newton's second law
Newton's second law states that the acceleration of a particle is proportional to the force acting on it. The acceleration of a particle is the rate of change of its velocity. The force acting on a particle is the product of its mass and acceleration. The force acting on a particle is equal to the rate of change of its momentum. The second law can be written as
$$
\mathbf{F}(x, y, z, t) = m \mathbf{a}(x, y, z, t)
$$
where $\mathbf{F}(t)=(F_x(t), F_y(t), F_z(t))$ is the force acting on the particle, $m$ is the mass of the particle, and
$\mathbf{a}=(x''(t), y''(t),z''(t))$ is the acceleration of the particle.
Above equation can be written as a set of three equations
$$
\begin{align*}
\frac{d^2 x(t)}{dt^2} &= \frac{F_x(x, y, z, t)}{m} \\
\frac{d^2 y(t)}{dt^2} &= \frac{F_y(x, y, z, t)}{m}\\
\frac{d^2 z(t)}{dt^2} &= \frac{F_z(x, y, z, t)}{m}
\end{align*}
$$
#### Bounduary conditions
To solve these equations we need to know the initial position and velocity of the particle. These are the boundary conditions of the problem. The boundary conditions are the initial values of the position and velocity of the particle. The boundary conditions determine the trajectory of the particle in space. The boundary conditions can be used to solve the differential equations of motion.
```{=html}
<div class="newton-dynamics-widget">
<style>
.newton-dynamics-widget {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
padding: 20px;
background-color: #fff;
border: 1px solid #e0e0e0;
border-radius: 8px;
color: #333;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
max-width: 850px;
margin: 20px auto;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
box-sizing: border-box;
user-select: none;
}
.newton-dynamics-widget h3 {
margin-top: 0;
color: #2c3e50;
font-size: 1.2em;
text-transform: uppercase;
letter-spacing: 1px;
text-align: center;
}
.newton-dynamics-widget .canvas-container {
position: relative;
margin: 10px 0 15px 0;
background: #fdfdfd;
border: 1px solid #ccc;
border-radius: 4px;
overflow: hidden;
cursor: crosshair;
}
.newton-dynamics-widget canvas {
display: block;
}
.newton-dynamics-widget .overlay-msg {
position: absolute;
top: 10px;
left: 50%;
transform: translateX(-50%);
background: rgba(255, 255, 255, 0.95);
padding: 6px 16px;
border-radius: 20px;
font-size: 0.9em;
font-weight: 600;
color: #333;
pointer-events: none;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: opacity 0.3s;
border: 1px solid #ddd;
z-index: 2;
}
.newton-dynamics-widget .field-formula {
position: absolute;
top: 10px;
right: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 6px 10px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-weight: bold;
font-size: 0.85em;
color: #c62828;
border: 1px solid #ddd;
pointer-events: none;
z-index: 1;
}
.newton-dynamics-widget .data-monitor {
position: absolute;
top: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 10px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 0.85em;
color: #333;
border: 1px solid #ddd;
pointer-events: none;
z-index: 1;
display: flex;
flex-direction: column;
gap: 4px;
min-width: 140px;
}
.newton-dynamics-widget .data-row {
display: flex;
justify-content: space-between;
}
.newton-dynamics-widget .data-label { font-weight: bold; margin-right: 8px; }
.newton-dynamics-widget .data-val { text-align: right; }
.newton-dynamics-widget .controls-panel {
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr 1fr auto;
gap: 15px;
align-items: center;
background: #f5f5f5;
padding: 15px;
border-radius: 6px;
border: 1px solid #eee;
}
.newton-dynamics-widget .control-group {
display: flex;
flex-direction: column;
gap: 5px;
}
.newton-dynamics-widget label {
font-weight: 600;
font-size: 0.8em;
color: #555;
white-space: nowrap;
}
.newton-dynamics-widget select {
padding: 6px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
cursor: pointer;
width: 100%;
font-size: 0.9em;
}
.newton-dynamics-widget input[type=range] {
width: 100%;
cursor: pointer;
}
.newton-dynamics-widget .btn-group {
display: flex;
gap: 10px;
}
.newton-dynamics-widget button {
padding: 0 16px;
border: none;
border-radius: 4px;
font-weight: bold;
font-size: 13px;
color: white;
cursor: pointer;
transition: background 0.2s, transform 0.1s;
height: 36px;
}
.newton-dynamics-widget .btn-reset {
background-color: #607d8b;
}
.newton-dynamics-widget .btn-reset:hover {
background-color: #455a64;
}
.newton-dynamics-widget .btn-replay {
background-color: #1976d2;
}
.newton-dynamics-widget .btn-replay:hover:not(:disabled) {
background-color: #1565c0;
}
.newton-dynamics-widget .btn-replay:disabled {
background-color: #bbdefb;
cursor: not-allowed;
}
.newton-dynamics-widget .legend {
width: 100%;
display: flex;
justify-content: center;
gap: 20px;
font-size: 0.85em;
margin-top: 5px;
padding-top: 10px;
border-top: 1px solid #e0e0e0;
flex-wrap: wrap;
}
.newton-dynamics-widget .legend-item {
display: flex;
align-items: center;
gap: 6px;
}
.newton-dynamics-widget .color-line { width: 20px; height: 3px; border-radius: 2px; }
.newton-dynamics-widget .color-dot { width: 10px; height: 10px; border-radius: 50%; }
@media (max-width: 800px) {
.newton-dynamics-widget .controls-panel {
grid-template-columns: 1fr 1fr;
}
}
</style>
<h3>Force Field Simulation (Newton's Laws)</h3>
<div class="canvas-container">
<canvas class="sim-canvas" width="800" height="450"></canvas>
<div class="overlay-msg">Click and drag to launch (Slingshot)</div>
<div class="field-formula">F = ...</div>
<div class="data-monitor">
<div class="data-row">
<span class="data-label">Time:</span>
<span class="data-val" id="val-t">0.00 s</span>
</div>
<div class="data-row" style="color:#1976d2">
<span class="data-label">Pos(r):</span>
<span class="data-val" id="val-r">[0, 0]</span>
</div>
<div class="data-row" style="color:#2e7d32">
<span class="data-label">Vel(v):</span>
<span class="data-val" id="val-v">[0, 0]</span>
</div>
<div class="data-row" style="color:#ef6c00">
<span class="data-label">Acc(a):</span>
<span class="data-val" id="val-a">[0, 0]</span>
</div>
</div>
</div>
<div class="controls-panel">
<div class="control-group">
<label>Force Field:</label>
<select class="field-select">
<option value="gravity">Gravity (Constant Down)</option>
<option value="sine">Sinusoidal</option>
<option value="decay">Linear Decay</option>
<option value="center">Central Point</option>
</select>
</div>
<div class="control-group">
<label>Field Intensity: <span class="val-strength" style="color:#1976d2">1.0x</span></label>
<input type="range" class="slider-strength" min="0" max="5.0" step="0.1" value="1.0">
</div>
<div class="control-group">
<label>Time Speed: <span class="val-speed" style="color:#555">1.0x</span></label>
<input type="range" class="slider-speed" min="0.1" max="1.0" step="0.1" value="1.0">
</div>
<div class="btn-group">
<button class="btn-replay" disabled>Replay</button>
<button class="btn-reset">Reset</button>
</div>
</div>
<div class="legend">
<div class="legend-item"><div class="color-dot" style="background: #d32f2f;"></div> Particle</div>
<div class="legend-item"><div class="color-line" style="background: #2e7d32;"></div> Velocity</div>
<div class="legend-item"><div class="color-line" style="background: #ef6c00;"></div> Acceleration</div>
<div class="legend-item"><div class="color-line" style="background: #aaa; border: 1px dashed #aaa;"></div> Path</div>
</div>
<script>
(function() {
const widget = document.currentScript.parentElement;
const canvas = widget.querySelector('.sim-canvas');
const ctx = canvas.getContext('2d');
const msgBox = widget.querySelector('.overlay-msg');
const formulaBox = widget.querySelector('.field-formula');
// Data Monitor Refs
const valT = widget.querySelector('#val-t');
const valR = widget.querySelector('#val-r');
const valV = widget.querySelector('#val-v');
const valA = widget.querySelector('#val-a');
const selectField = widget.querySelector('.field-select');
const sliderStrength = widget.querySelector('.slider-strength');
const valStrength = widget.querySelector('.val-strength');
const sliderSpeed = widget.querySelector('.slider-speed');
const valSpeed = widget.querySelector('.val-speed');
const btnReset = widget.querySelector('.btn-reset');
const btnReplay = widget.querySelector('.btn-replay');
const config = {
baseDt: 0.1,
mass: 1.0,
gridSpacing: 50,
wallBounce: 1.0,
radius: 6,
dragForceFactor: 0.15,
vectorVisualScale: 3.0
};
const fields = {
gravity: {
// Gravity pulls DOWN (Negative Y in Cartesian)
fn: (x, y) => ({ x: 0, y: -5 }),
maxRef: 5,
formula: "F = [0, -5]"
},
sine: {
fn: (x, y) => ({ x: 0, y: 10 * Math.sin(x / 50) }),
maxRef: 10,
formula: "F = [0, 10·sin(x/50)]"
},
decay: {
// Linear Decay: Strong at bottom (y=0), Zero at top (y=h)
// Force is Down (negative)
fn: (x, y) => {
const h = canvas.height;
// Physics y=0 is bottom.
const normalizedY = y / h;
// Strength decreases as we go up
const strength = 15 * (1 - normalizedY);
// Force is downwards
return { x: 0, y: -Math.max(0, strength) };
},
maxRef: 15,
formula: "F = [0, -15·(1 - y/h)]"
},
center: {
fn: (x, y) => {
const cx = canvas.width / 2;
const cy = canvas.height / 2;
const dx = cx - x;
const dy = cy - y;
const dist = Math.sqrt(dx*dx + dy*dy);
if (dist < 5) return {x:0, y:0};
const F = 10 * (dist / 300);
return { x: F * (dx/dist), y: F * (dy/dist) };
},
maxRef: 10,
formula: "F ~ Dist to Center"
}
};
let state = {
status: 'IDLE',
fieldKey: 'gravity',
strengthMultiplier: 1.0,
timeSpeed: 1.0,
timeElapsed: 0,
// PHYSICS COORDINATES (0,0 at Bottom-Left)
pos: { x: 100, y: 100 },
vel: { x: 0, y: 0 },
acc: { x: 0, y: 0 },
path: [],
// MOUSE / SCREEN INTERACTION
dragStartPhysics: { x: 0, y: 0 },
dragCurrentPhysics: { x: 0, y: 0 },
lastShot: {
exists: false,
startPos: { x: 0, y: 0 },
startVel: { x: 0, y: 0 }
},
animId: null
};
// --- Coordinate Helpers ---
// Converts Physics Coords (Y-Up) to Screen Coords (Y-Down, 0 at top)
function toScreen(x, y) {
return {
x: x,
y: canvas.height - y
};
}
// Converts Screen Coords (MouseEvent) to Physics Coords (Y-Up)
function toPhysics(screenX, screenY) {
return {
x: screenX,
y: canvas.height - screenY
};
}
function getForce(x, y) {
const field = fields[state.fieldKey];
const baseF = field.fn(x, y);
return {
x: baseF.x * state.strengthMultiplier,
y: baseF.y * state.strengthMultiplier
};
}
function updatePhysics() {
const dt = config.baseDt * state.timeSpeed;
state.timeElapsed += dt;
const F = getForce(state.pos.x, state.pos.y);
const a = { x: F.x / config.mass, y: F.y / config.mass };
state.acc = a;
// Euler-Cromer
state.vel.x += a.x * dt;
state.vel.y += a.y * dt;
state.pos.x += state.vel.x * dt;
state.pos.y += state.vel.y * dt;
if (state.animId % (Math.floor(3/state.timeSpeed)) === 0) {
state.path.push({ ...state.pos });
}
// Wall Bounce
const r = config.radius;
const w = canvas.width;
const h = canvas.height;
const bounce = config.wallBounce;
if (state.pos.x < r) {
state.pos.x = r;
state.vel.x *= -bounce;
} else if (state.pos.x > w - r) {
state.pos.x = w - r;
state.vel.x *= -bounce;
}
// Ground/Ceiling
if (state.pos.y < r) {
state.pos.y = r;
state.vel.y *= -bounce;
} else if (state.pos.y > h - r) {
state.pos.y = h - r;
state.vel.y *= -bounce;
}
}
function updateDataMonitor() {
valT.innerText = state.timeElapsed.toFixed(2) + ' s';
const fmt = (v) => `[${v.x.toFixed(0)}, ${v.y.toFixed(0)}]`;
if (state.status === 'RUNNING' || state.status === 'DRAGGING') {
valR.innerText = fmt(state.pos);
valV.innerText = fmt(state.vel);
valA.innerText = fmt(state.acc);
} else if (state.status === 'IDLE' && state.lastShot.exists) {
valR.innerText = fmt(state.lastShot.startPos);
valV.innerText = fmt(state.lastShot.startVel);
valA.innerText = "[0, 0]";
}
}
function drawAxes() {
ctx.save();
ctx.strokeStyle = '#333';
ctx.lineWidth = 1;
ctx.fillStyle = '#333';
ctx.font = '10px monospace';
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
// Y Axis (Logic X=0)
const p0 = toScreen(0, 0);
const pTop = toScreen(0, canvas.height);
ctx.beginPath();
ctx.moveTo(p0.x, p0.y);
ctx.lineTo(pTop.x, pTop.y);
ctx.stroke();
for(let y=0; y<=canvas.height; y+=50) {
const s = toScreen(0, y);
ctx.beginPath();
ctx.moveTo(s.x, s.y);
ctx.lineTo(s.x + 6, s.y);
ctx.stroke();
if(y > 0) ctx.fillText(y, s.x + 30, s.y);
}
// X Axis (Logic Y=0 is at bottom of screen)
const pRight = toScreen(canvas.width, 0);
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.beginPath();
ctx.moveTo(p0.x, p0.y);
ctx.lineTo(pRight.x, pRight.y);
ctx.stroke();
for(let x=0; x<=canvas.width; x+=50) {
const s = toScreen(x, 0);
ctx.beginPath();
ctx.moveTo(s.x, s.y);
ctx.lineTo(s.x, s.y - 6);
ctx.stroke();
if(x > 0) ctx.fillText(x, s.x, s.y - 15);
}
ctx.font = 'bold 12px sans-serif';
const xLabel = toScreen(canvas.width - 40, 0);
ctx.fillText("X (px)", xLabel.x, xLabel.y - 30);
ctx.save();
const yLabel = toScreen(0, canvas.height - 40);
ctx.translate(yLabel.x + 15, yLabel.y);
ctx.rotate(-Math.PI/2);
ctx.fillText("Y (px)", 0, 0);
ctx.restore();
ctx.restore();
}
function drawArrow(physX, physY, vx, vy, color, lineWidth = 2, alpha = 1.0) {
const len = Math.sqrt(vx*vx + vy*vy);
if (len < 1) return;
// Convert Start Point to Screen
const start = toScreen(physX, physY);
// Calculate End Point in Physics, then Convert to Screen
// (Note: Canvas Y is inverted, so physics Up is screen Up (smaller Y))
const endPhysX = physX + vx;
const endPhysY = physY + vy;
const end = toScreen(endPhysX, endPhysY);
const dx = end.x - start.x;
const dy = end.y - start.y;
const angle = Math.atan2(dy, dx);
const headLen = 8;
ctx.save();
ctx.globalAlpha = alpha;
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.lineWidth = lineWidth;
ctx.beginPath(); ctx.moveTo(start.x, start.y); ctx.lineTo(end.x, end.y); ctx.stroke();
ctx.beginPath(); ctx.moveTo(end.x, end.y);
ctx.lineTo(end.x - headLen * Math.cos(angle - Math.PI/6), end.y - headLen * Math.sin(angle - Math.PI/6));
ctx.lineTo(end.x - headLen * Math.cos(angle + Math.PI/6), end.y - headLen * Math.sin(angle + Math.PI/6));
ctx.lineTo(end.x, end.y); ctx.fill();
ctx.restore();
}
function drawField() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawAxes();
const spacing = config.gridSpacing;
const currentField = fields[state.fieldKey];
for (let x = spacing/2; x < canvas.width; x += spacing) {
for (let y = spacing/2; y < canvas.height; y += spacing) {
const baseF = currentField.fn(x, y);
const Fx = baseF.x * state.strengthMultiplier;
const Fy = baseF.y * state.strengthMultiplier;
const mag = Math.sqrt(Fx*Fx + Fy*Fy);
const refMag = currentField.maxRef * Math.max(1, state.strengthMultiplier);
if (mag > 0.05) {
const scale = 25 / refMag;
drawArrow(x, y, Fx * scale, Fy * scale, '#cfd8dc', 1.5, 0.6);
} else {
const s = toScreen(x, y);
ctx.fillStyle = '#eceff1'; ctx.fillRect(s.x-1, s.y-1, 2, 2);
}
}
}
}
function drawScene() {
drawField();
// Ghost of last shot
if (state.status === 'IDLE' && state.lastShot.exists) {
const ls = state.lastShot;
const s = toScreen(ls.startPos.x, ls.startPos.y);
ctx.save();
ctx.globalAlpha = 0.4;
ctx.beginPath(); ctx.fillStyle = '#d32f2f';
ctx.arc(s.x, s.y, config.radius, 0, Math.PI*2);
ctx.fill();
drawArrow(ls.startPos.x, ls.startPos.y, ls.startVel.x * config.vectorVisualScale, ls.startVel.y * config.vectorVisualScale, '#2e7d32', 2, 0.4);
ctx.restore();
}
// Path
if (state.path.length > 1) {
ctx.beginPath();
ctx.strokeStyle = '#78909c';
ctx.lineWidth = 2;
ctx.setLineDash([4, 4]);
const start = toScreen(state.path[0].x, state.path[0].y);
ctx.moveTo(start.x, start.y);
for (let i = 1; i < state.path.length; i++) {
const p = toScreen(state.path[i].x, state.path[i].y);
ctx.lineTo(p.x, p.y);
}
ctx.stroke();
ctx.setLineDash([]);
}
// DRAGGING
if (state.status === 'DRAGGING') {
const startS = toScreen(state.dragStartPhysics.x, state.dragStartPhysics.y);
const currS = toScreen(state.dragCurrentPhysics.x, state.dragCurrentPhysics.y);
// Slingshot line
ctx.beginPath();
ctx.strokeStyle = '#444'; ctx.lineWidth = 1.5; ctx.setLineDash([3, 3]);
ctx.moveTo(startS.x, startS.y);
ctx.lineTo(currS.x, currS.y);
ctx.stroke(); ctx.setLineDash([]);
// Physics Vector: Start - Current
const vx = (state.dragStartPhysics.x - state.dragCurrentPhysics.x) * config.dragForceFactor;
const vy = (state.dragStartPhysics.y - state.dragCurrentPhysics.y) * config.dragForceFactor;
drawArrow(state.dragStartPhysics.x, state.dragStartPhysics.y, vx * config.vectorVisualScale, vy * config.vectorVisualScale, '#2e7d32', 2);
ctx.beginPath(); ctx.fillStyle = '#d32f2f';
ctx.arc(startS.x, startS.y, config.radius, 0, Math.PI*2); ctx.fill();
}
// RUNNING
if (state.status === 'RUNNING') {
const p = state.pos;
const s = toScreen(p.x, p.y);
ctx.beginPath(); ctx.fillStyle = '#d32f2f';
ctx.arc(s.x, s.y, config.radius, 0, Math.PI*2); ctx.fill();
drawArrow(p.x, p.y, state.vel.x * config.vectorVisualScale, state.vel.y * config.vectorVisualScale, '#2e7d32', 2);
const F = getForce(p.x, p.y);
// Draw acceleration vector
drawArrow(p.x, p.y, (F.x/config.mass) * 4, (F.y/config.mass) * 4, '#ef6c00', 2);
}
updateDataMonitor();
}
function loop() {
if (state.status !== 'RUNNING') return;
updatePhysics();
drawScene();
state.animId = requestAnimationFrame(loop);
}
canvas.addEventListener('mousedown', (e) => {
if (state.status === 'RUNNING') cancelAnimationFrame(state.animId);
const rect = canvas.getBoundingClientRect();
const sx = e.clientX - rect.left;
const sy = e.clientY - rect.top;
const phys = toPhysics(sx, sy);
state.status = 'DRAGGING';
state.dragStartPhysics = phys;
state.dragCurrentPhysics = phys;
state.pos = phys;
state.vel = { x: 0, y: 0 };
state.acc = { x: 0, y: 0 };
state.timeElapsed = 0;
state.path = [];
msgBox.style.opacity = '0';
drawScene();
});
canvas.addEventListener('mousemove', (e) => {
if (state.status !== 'DRAGGING') return;
const rect = canvas.getBoundingClientRect();
const sx = e.clientX - rect.left;
const sy = e.clientY - rect.top;
state.dragCurrentPhysics = toPhysics(sx, sy);
// Live update for dragging
state.pos = state.dragStartPhysics;
updateDataMonitor();
drawScene();
});
canvas.addEventListener('mouseup', (e) => {
if (state.status !== 'DRAGGING') return;
state.vel = {
x: (state.dragStartPhysics.x - state.dragCurrentPhysics.x) * config.dragForceFactor,
y: (state.dragStartPhysics.y - state.dragCurrentPhysics.y) * config.dragForceFactor
};
state.lastShot.exists = true;
state.lastShot.startPos = { ...state.dragStartPhysics };
state.lastShot.startVel = { ...state.vel };
btnReplay.disabled = false;
btnReplay.innerText = "Replay Shot";
state.status = 'RUNNING';
loop();
});
selectField.addEventListener('change', (e) => {
state.fieldKey = e.target.value;
reset();
});
sliderStrength.addEventListener('input', (e) => {
state.strengthMultiplier = parseFloat(e.target.value);
valStrength.innerText = state.strengthMultiplier.toFixed(1) + 'x';
if (state.status === 'IDLE') drawScene();
});
sliderSpeed.addEventListener('input', (e) => {
state.timeSpeed = parseFloat(e.target.value);
valSpeed.innerText = Math.round(state.timeSpeed * 100) + '%';
});
btnReset.addEventListener('click', reset);
btnReplay.addEventListener('click', () => {
if (!state.lastShot.exists) return;
if (state.status === 'RUNNING') cancelAnimationFrame(state.animId);
state.pos = { ...state.lastShot.startPos };
state.vel = { ...state.lastShot.startVel };
state.timeElapsed = 0;
state.path = [];
msgBox.style.opacity = '0';
state.status = 'RUNNING';
loop();
});
function reset() {
state.status = 'IDLE';
cancelAnimationFrame(state.animId);
state.path = [];
state.pos = { x: 400, y: 225 }; // Center ish
state.timeElapsed = 0;
msgBox.innerText = "Click and drag to launch (Slingshot)";
msgBox.style.opacity = '1';
formulaBox.innerText = fields[state.fieldKey].formula;
drawScene();
}
formulaBox.innerText = fields[state.fieldKey].formula;
drawScene();
})();
</script>
</div>
```
### Newton's third law
Newton's third law states that forces always occur in pairs. If one object exerts a force on another object, the second object exerts an equal and opposite force on the first object. This law is also known as the law of action and reaction. The law of action and reaction states that for every action there is an equal and opposite reaction. This law is a consequence of the conservation of momentum. The momentum of a system is conserved if no external forces act on it.
### Solving the Second-Order Equation Iteratively
#### 1. Rewriting the Second-Order Equation
The second-order equation is:
$$
\frac{d^2 x(t)}{dt^2} = \frac{F_x(x, y, z, t)}{m}.
$$
We introduce $v_x(t)$, the velocity in the $x$-direction, such that:
$$
v_x(t) = \frac{dx(t)}{dt}.
$$
This converts the equation into a system of two first-order equations:
$$
\begin{align}
\frac{dx(t)}{dt} &= v_x(t), \\
&\\
\frac{dv_x(t)}{dt} &= \frac{F_x(x, y, z, t)}{m}.
\end{align}
$$
#### 2. Iterative Procedure
We solve these equations step-by-step using numerical methods. For simplicity, let's use **Euler's method**:
- Let $x_n = x(t_n)$ and $v_{x,n} = v_x(t_n)$.
- Given a small time step $\Delta t$, the update rules are:
$$
\begin{align}
x_{n+1} &= x_n + v_{x,n} \Delta t, \\
v_{x,n+1} &= v_{x,n} + \frac{F_x(x_n, y_n, z_n, t_n)}{m} \Delta t.
\end{align}
$$
We iterate these equations to compute the trajectory.
#### 3. Focus on the $x$-Component
To make this concrete, let's assume:
- A force $F_x(x, y, z, t) = -kx^3$, where $k$ is a constant.
- We ignore $y$ and $z$ for simplicity (focus on $x$-component only).
This gives:
$$
\frac{dv_x(t)}{dt} = \frac{-k (x(t))^3}{m}.
$$
The iterative update rules become:
$$
\begin{cases}
x_{n+1} = x_n + v_{x,n} \Delta t, \\
v_{x,n+1} = v_{x,n} - \frac{k x_n^3}{m} \Delta t.
\end{cases}
$$
#### 4. Numerical Implementation
Here's how we can implement this:
```{python}
import numpy as np
import matplotlib.pyplot as plt
# Parameters
k = 1.0 # Spring constant
m = 1.0 # Mass
dt = 0.01 # Time step
steps = 1000 # Number of steps
x0 = 1.0 # Initial position
v0 = 0.0 # Initial velocity
def force(x):
return -k * x*x*x
# Arrays to store time, position, and velocity
time = np.linspace(0, steps * dt, steps)
x = np.zeros(steps)
v = np.zeros(steps)
# Initial conditions
x[0] = x0
v[0] = v0
# Iterative solution using Euler's method
for n in range(steps - 1):
# Update position
x[n + 1] = x[n] + v[n] * dt
# Update velocity
v[n + 1] = v[n] + force(x[n]) / m * dt
# Plot results
plt.figure(figsize=(10, 5))
plt.plot(time, x, label='Position (x)')
plt.plot(time, v, label='Velocity (v)')
plt.xlabel('Time (t)')
plt.ylabel('Position/Velocity')
plt.title('Harmonic Oscillator Solution')
plt.legend()
plt.grid()
plt.show()
```
```{=html}
<div class="numerical-oscillator-widget">
<style>
.numerical-oscillator-widget {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
padding: 20px;
background-color: #fff;
border: 1px solid #e0e0e0;
border-radius: 8px;
color: #333;
display: flex;
flex-direction: column;
gap: 15px;
width: 100%;
max-width: 950px;
margin: 20px auto;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
box-sizing: border-box;
user-select: none;
}
.numerical-oscillator-widget h3 {
margin: 0;
color: #2c3e50;
font-size: 1.2em;
text-align: center;
text-transform: uppercase;
letter-spacing: 1px;
}
.numerical-oscillator-widget .main-layout {
display: grid;
grid-template-columns: 300px 1fr;
gap: 20px;
}
/* Panel Matematyczny */
.numerical-oscillator-widget .math-panel {
background: #f8f9fa;
border: 1px solid #dae0e5;
border-radius: 6px;
padding: 15px;
font-family: 'Courier New', monospace;
font-size: 0.85em;
display: flex;
flex-direction: column;
gap: 10px;
height: fit-content;
}
.numerical-oscillator-widget .math-step {
margin-bottom: 5px;
padding-bottom: 5px;
border-bottom: 1px dashed #ccc;
}
.numerical-oscillator-widget .math-label {
font-weight: bold;
color: #555;
margin-bottom: 4px;
display: block;
font-family: sans-serif;
}
.numerical-oscillator-widget .math-eq {
color: #1565c0;
background: #fff;
padding: 4px;
border-radius: 3px;
border: 1px solid #eee;
display: block;
margin-top: 2px;
white-space: pre-wrap; /* allow wrapping */
}
.numerical-oscillator-widget .math-val {
color: #d32f2f;
font-weight: bold;
}
/* Prawa kolumna */
.numerical-oscillator-widget .vis-column {
display: flex;
flex-direction: column;
gap: 10px;
}
/* Fizyka */
.numerical-oscillator-widget .phys-container {
position: relative;
background: #f9f9f9;
border: 1px solid #ccc;
border-radius: 4px;
height: 140px;
width: 100%;
}
.numerical-oscillator-widget .phys-canvas {
display: block;
width: 100%;
height: 100%;
}
/* Wykresy */
.numerical-oscillator-widget .charts-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 8px;
height: 180px;
}
.numerical-oscillator-widget .chart-box {
border: 1px solid #ddd;
background: #fff;
border-radius: 4px;
display: flex;
flex-direction: column;
position: relative;
}
.numerical-oscillator-widget .chart-title {
font-size: 0.75em;
font-weight: bold;
text-align: center;
padding: 4px;
background: #f0f0f0;
border-bottom: 1px solid #ddd;
}
.numerical-oscillator-widget .chart-box canvas {
display: block;
width: 100%;
height: 100%;
min-height: 0;
}
/* Panel sterowania */
.numerical-oscillator-widget .controls-panel {
grid-column: 1 / -1;
background: #eee;
padding: 15px;
border-radius: 6px;
display: flex;
flex-wrap: wrap;
gap: 15px;
justify-content: center;
align-items: center;
}
.numerical-oscillator-widget .control-group {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 100px;
}
.numerical-oscillator-widget label {
font-size: 0.8em;
font-weight: 700;
color: #444;
}
.numerical-oscillator-widget select {
padding: 6px;
border-radius: 4px;
border: 1px solid #ccc;
font-weight: bold;
}
.numerical-oscillator-widget button {
padding: 10px 18px;
border: none;
border-radius: 4px;
font-weight: bold;
font-size: 13px;
color: white;
cursor: pointer;
transition: transform 0.1s;
}
.numerical-oscillator-widget .btn-step { background-color: #f57f17; }
.numerical-oscillator-widget .btn-run { background-color: #2e7d32; }
.numerical-oscillator-widget .btn-reset { background-color: #607d8b; }
.numerical-oscillator-widget button:hover { transform: scale(1.05); filter: brightness(1.1); }
@media (max-width: 800px) {
.numerical-oscillator-widget .main-layout {
grid-template-columns: 1fr;
}
.numerical-oscillator-widget .charts-grid {
height: auto;
grid-template-columns: 1fr;
}
.numerical-oscillator-widget .chart-box {
height: 140px;
}
}
</style>
<h3>Numerical Solution: Nonlinear Oscillator (F = -kx³)</h3>
<div class="main-layout">
<!-- Lewa kolumna: Wzory -->
<div class="math-panel">
<div class="math-step">
<span class="math-label">1. Integration Method:</span>
<div class="math-eq" id="math-method-name" style="font-weight:bold; color:#333;">Euler (1st Order)</div>
</div>
<div class="math-step">
<span class="math-label">2. Current State (n):</span>
<div class="math-eq">t = <span id="m-t" class="math-val">0.00</span></div>
<div class="math-eq">x = <span id="m-x" class="math-val">1.00</span></div>
<div class="math-eq">v = <span id="m-v" class="math-val">0.00</span></div>
</div>
<div class="math-step" id="math-euler-block">
<span class="math-label">3. Euler Step (n+1):</span>
<div class="math-eq">a<sub>n</sub> = -k·x<sub>n</sub>³ / m</div>
<div class="math-eq">x<sub>n+1</sub> = x<sub>n</sub> + v<sub>n</sub>·dt</div>
<div class="math-eq">= <span id="m-x-calc">...</span></div>
<div class="math-eq">v<sub>n+1</sub> = v<sub>n</sub> + a<sub>n</sub>·dt</div>
<div class="math-eq">= <span id="m-v-calc">...</span></div>
</div>
<div class="math-step" id="math-rk4-block" style="display:none;">
<span class="math-label">3. RK4 Step (Summary):</span>
<div class="math-eq">k1, k2, k3, k4 calculated...</div>
<div class="math-eq">x<sub>n+1</sub> = x<sub>n</sub> + <span style="font-size:0.8em">1/6</span>(k1<sub>x</sub>..k4<sub>x</sub>)dt</div>
<div class="math-eq">= <span id="m-rk-x">...</span></div>
<div class="math-eq">v<sub>n+1</sub> = v<sub>n</sub> + <span style="font-size:0.8em">1/6</span>(k1<sub>v</sub>..k4<sub>v</sub>)dt</div>
<div class="math-eq">= <span id="m-rk-v">...</span></div>
</div>
</div>
<!-- Prawa kolumna: Wizualizacja -->
<div class="vis-column">
<!-- Kulka -->
<div class="phys-container">
<canvas class="phys-canvas"></canvas>
</div>
<!-- Wykresy -->
<div class="charts-grid">
<div class="chart-box">
<div class="chart-title" style="color:#1976d2">Position x(t)</div>
<canvas class="chart-x"></canvas>
</div>
<div class="chart-box">
<div class="chart-title" style="color:#2e7d32">Velocity v(t)</div>
<canvas class="chart-v"></canvas>
</div>
<div class="chart-box">
<div class="chart-title" style="color:#ef6c00">Acceleration a(t)</div>
<canvas class="chart-a"></canvas>
</div>
</div>
</div>
<!-- Sterowanie -->
<div class="controls-panel">
<div class="control-group">
<label>Method:</label>
<select id="select-method">
<option value="euler">Euler (Unstable)</option>
<option value="rk4">Runge-Kutta 4 (Stable)</option>
</select>
</div>
<div class="control-group">
<label>Time Step (Δt): <span id="lbl-dt">0.05</span></label>
<input type="range" id="slider-dt" min="0.01" max="0.2" step="0.01" value="0.05">
</div>
<div class="control-group">
<label>Constant (k): <span id="lbl-k">1.0</span></label>
<input type="range" id="slider-k" min="0.1" max="5.0" step="0.1" value="1.0">
</div>
<div class="control-group">
<label>Anim Speed: <span id="lbl-speed">Med</span></label>
<input type="range" id="slider-speed" min="1" max="20" step="1" value="5">
</div>
<button class="btn-step">Step</button>
<button class="btn-run">Run / Stop</button>
<button class="btn-reset">Reset</button>
</div>
</div>
<script>
(function() {
const widget = document.currentScript.parentElement;
// Refs
const physCanvas = widget.querySelector('.phys-canvas');
const chartXCanvas = widget.querySelector('.chart-x');
const chartVCanvas = widget.querySelector('.chart-v');
const chartACanvas = widget.querySelector('.chart-a');
const ctxPhys = physCanvas.getContext('2d');
const ctxX = chartXCanvas.getContext('2d');
const ctxV = chartVCanvas.getContext('2d');
const ctxA = chartACanvas.getContext('2d');
// Math Panel UI
const mathMethodName = widget.querySelector('#math-method-name');
const mathEulerBlock = widget.querySelector('#math-euler-block');
const mathRk4Block = widget.querySelector('#math-rk4-block');
const m_t = widget.querySelector('#m-t');
const m_x = widget.querySelector('#m-x');
const m_v = widget.querySelector('#m-v');
const m_x_calc = widget.querySelector('#m-x-calc');
const m_v_calc = widget.querySelector('#m-v-calc');
const m_rk_x = widget.querySelector('#m-rk-x');
const m_rk_v = widget.querySelector('#m-rk-v');
// Controls
const selectMethod = widget.querySelector('#select-method');
const sliderDt = widget.querySelector('#slider-dt');
const lblDt = widget.querySelector('#lbl-dt');
const sliderK = widget.querySelector('#slider-k');
const lblK = widget.querySelector('#lbl-k');
const sliderSpeed = widget.querySelector('#slider-speed');
const btnStep = widget.querySelector('.btn-step');
const btnRun = widget.querySelector('.btn-run');
const btnReset = widget.querySelector('.btn-reset');
// State
let state = {
t: 0,
x: 1.0,
v: 0,
a: 0,
k: 1.0,
m: 1.0,
dt: 0.05,
history: [],
isRunning: false,
animId: null,
method: 'euler' // 'euler' or 'rk4'
};
function resizeCanvases() {
[physCanvas, chartXCanvas, chartVCanvas, chartACanvas].forEach(c => {
c.width = c.clientWidth || 300;
c.height = c.clientHeight || 150;
});
drawAll();
}
window.addEventListener('resize', resizeCanvases);
// --- MATH LOGIC ---
function getAcc(x, v) {
// F = -k * x^3
// a = F / m
return (-state.k * Math.pow(x, 3)) / state.m;
}
function calculateEuler() {
const currentAcc = getAcc(state.x, state.v);
state.a = currentAcc; // Log current acceleration
const nextX = state.x + state.v * state.dt;
const nextV = state.v + currentAcc * state.dt;
// UI Updates
m_x_calc.innerText = `${state.x.toFixed(2)} + (${state.v.toFixed(2)} * ${state.dt}) = ${nextX.toFixed(3)}`;
m_v_calc.innerText = `${state.v.toFixed(2)} + (${currentAcc.toFixed(2)} * ${state.dt}) = ${nextV.toFixed(3)}`;
return { x: nextX, v: nextV };
}
function calculateRK4() {
const dt = state.dt;
const x = state.x;
const v = state.v;
// k1
const a1 = getAcc(x, v);
const k1v = a1 * dt;
const k1x = v * dt;
// k2
const a2 = getAcc(x + k1x/2, v + k1v/2);
const k2v = a2 * dt;
const k2x = (v + k1v/2) * dt;
// k3
const a3 = getAcc(x + k2x/2, v + k2v/2);
const k3v = a3 * dt;
const k3x = (v + k2v/2) * dt;
// k4
const a4 = getAcc(x + k3x, v + k3v);
const k4v = a4 * dt;
const k4x = (v + k3v) * dt;
const nextX = x + (k1x + 2*k2x + 2*k3x + k4x) / 6;
const nextV = v + (k1v + 2*k2v + 2*k3v + k4v) / 6;
state.a = a1; // Display acceleration at start of step
// UI
m_rk_x.innerText = nextX.toFixed(3);
m_rk_v.innerText = nextV.toFixed(3);
return { x: nextX, v: nextV };
}
function step() {
let nextState;
// Update Math Panel displays
m_t.innerText = state.t.toFixed(3);
m_x.innerText = state.x.toFixed(3);
m_v.innerText = state.v.toFixed(3);
if (state.method === 'euler') {
mathEulerBlock.style.display = 'block';
mathRk4Block.style.display = 'none';
mathMethodName.innerText = "Euler (Explicit)";
mathMethodName.style.color = "#d32f2f"; // Warning color
nextState = calculateEuler();
} else {
mathEulerBlock.style.display = 'none';
mathRk4Block.style.display = 'block';
mathMethodName.innerText = "Runge-Kutta 4";
mathMethodName.style.color = "#2e7d32"; // Good color
nextState = calculateRK4();
}
// Commit
state.x = nextState.x;
state.v = nextState.v;
state.t += state.dt;
state.history.push({
t: state.t,
x: state.x,
v: state.v,
a: state.a
});
if (state.history.length > 500) state.history.shift();
}
// --- DRAWING ---
function drawAxes(ctx, width, height, yMin, yMax) {
ctx.clearRect(0, 0, width, height);
// Grid lines
ctx.strokeStyle = '#f0f0f0';
ctx.lineWidth = 1;
// Zero line
if (yMin < 0 && yMax > 0) {
const zeroY = map(0, yMin, yMax, height, 0);
ctx.beginPath();
ctx.strokeStyle = '#ccc';
ctx.moveTo(35, zeroY); // Leave space for labels
ctx.lineTo(width, zeroY);
ctx.stroke();
}
// Scale Labels
ctx.fillStyle = '#666';
ctx.font = '10px monospace';
ctx.textAlign = 'right';
// Top
ctx.fillText(yMax.toFixed(1), 30, 10);
// Middle
const mid = (yMin + yMax) / 2;
ctx.fillText(mid.toFixed(1), 30, height/2 + 3);
// Bottom
ctx.fillText(yMin.toFixed(1), 30, height - 5);
// Vertical line divider
ctx.beginPath();
ctx.strokeStyle = '#ccc';
ctx.moveTo(35, 0);
ctx.lineTo(35, height);
ctx.stroke();
}
function drawGraph(ctx, data, key, color) {
const w = ctx.canvas.width;
const h = ctx.canvas.height;
// Fixed range fallback, but lets try semi-dynamic
// To show instability, allow scale to grow
// Default range for start
let yMax = 2.0;
let yMin = -2.0;
if (key === 'v') { yMax = 4.0; yMin = -4.0; }
if (key === 'a') { yMax = 10.0; yMin = -10.0; }
// Check actual data max to expand scale if blowing up
if (data.length > 0) {
let dMax = -Infinity;
let dMin = Infinity;
// Only check visible window
const lookback = Math.min(data.length, 300);
for(let i=data.length-lookback; i<data.length; i++) {
const val = data[i][key];
if (val > dMax) dMax = val;
if (val < dMin) dMin = val;
}
// Expand range if needed
if (dMax > yMax) yMax = dMax * 1.2;
if (dMin < yMin) yMin = dMin * 1.2;
// Keep symmetrical for oscillator
const absMax = Math.max(Math.abs(yMax), Math.abs(yMin));
yMax = absMax;
yMin = -absMax;
}
drawAxes(ctx, w, h, yMin, yMax);
if (data.length < 2) return;
ctx.beginPath();
ctx.strokeStyle = color;
ctx.lineWidth = 2;
const maxPoints = 300;
const startIndex = Math.max(0, data.length - maxPoints);
const count = data.length - startIndex;
const drawAreaX = 35; // Offset for labels
const drawWidth = w - drawAreaX;
for (let i = 0; i < count; i++) {
const idx = startIndex + i;
const val = data[idx][key];
// Map x
const px = drawAreaX + (i / maxPoints) * drawWidth;
// Map y
const py = map(val, yMin, yMax, h, 0);
if (i===0) ctx.moveTo(px, py);
else ctx.lineTo(px, py);
}
ctx.stroke();
// Current dot
const lastIdx = count - 1;
const lastVal = data[startIndex + lastIdx][key];
const lx = drawAreaX + (lastIdx / maxPoints) * drawWidth;
const ly = map(lastVal, yMin, yMax, h, 0);
ctx.fillStyle = color;
ctx.beginPath(); ctx.arc(lx, ly, 3, 0, Math.PI*2); ctx.fill();
}
function drawPhysics() {
const w = physCanvas.width;
const h = physCanvas.height;
const cy = h / 2;
const cx = w / 2;
const scale = 60;
ctxPhys.clearRect(0, 0, w, h);
// Equilibrium
ctxPhys.strokeStyle = '#ddd';
ctxPhys.setLineDash([5, 5]);
ctxPhys.beginPath(); ctxPhys.moveTo(cx, 0); ctxPhys.lineTo(cx, h); ctxPhys.stroke();
ctxPhys.setLineDash([]);
// Limit visualization range so ball doesn't fly off screen instantly
let visX = state.x;
if (visX > 5) visX = 5;
if (visX < -5) visX = -5;
const bx = cx + visX * scale;
// Spring
ctxPhys.strokeStyle = '#555';
ctxPhys.lineWidth = 2;
ctxPhys.beginPath();
ctxPhys.moveTo(0, cy);
const segs = 20;
const step = bx / segs;
for(let i=0; i<=segs; i++) {
const yOffset = (i%2===0) ? 0 : (i%4===1 ? -10 : 10);
if(i===0 || i===segs) ctxPhys.lineTo(i*step, cy);
else ctxPhys.lineTo(i*step, cy + yOffset);
}
ctxPhys.stroke();
// Ball
ctxPhys.fillStyle = '#1976d2';
ctxPhys.beginPath();
ctxPhys.arc(bx, cy, 15, 0, Math.PI*2);
ctxPhys.fill();
// Vectors
// v
if(Math.abs(state.v) > 0.01) drawArrow(ctxPhys, bx, cy - 25, state.v * 15, '#2e7d32');
// a
if(Math.abs(state.a) > 0.01) drawArrow(ctxPhys, bx, cy + 25, state.a * 15, '#ef6c00');
}
function drawArrow(ctx, x, y, len, color) {
// Limit visual length
if (len > 100) len = 100;
if (len < -100) len = -100;
ctx.save();
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + len, y);
ctx.stroke();
const angle = len > 0 ? 0 : Math.PI;
const head = 8;
ctx.beginPath();
ctx.moveTo(x + len, y);
ctx.lineTo(x + len - head*Math.cos(angle-Math.PI/6), y - head*Math.sin(angle-Math.PI/6));
ctx.lineTo(x + len - head*Math.cos(angle+Math.PI/6), y - head*Math.sin(angle+Math.PI/6));
ctx.fill();
ctx.restore();
}
function map(val, inMin, inMax, outMin, outMax) {
return (val - inMin) * (outMax - outMin) / (inMax - inMin) + outMin;
}
function drawAll() {
drawPhysics();
drawGraph(ctxX, state.history, 'x', '#1976d2');
drawGraph(ctxV, state.history, 'v', '#2e7d32');
drawGraph(ctxA, state.history, 'a', '#ef6c00');
}
// --- Loop ---
function loop() {
if (!state.isRunning) return;
const speedVal = parseInt(sliderSpeed.value);
let steps = 1;
if (speedVal > 5) {
steps = (speedVal - 5) * 2;
for(let i=0; i<steps; i++) step();
} else {
state.frameCount = (state.frameCount || 0) + 1;
const skip = (6 - speedVal) * 3;
if (state.frameCount % skip === 0) step();
else {
state.animId = requestAnimationFrame(loop);
return;
}
}
drawAll();
state.animId = requestAnimationFrame(loop);
}
// Events
selectMethod.addEventListener('change', (e) => {
state.method = e.target.value;
// Redraw to update math panel label
drawAll();
// Trigger a dummy math update without stepping to refresh labels
if (!state.isRunning && state.history.length === 0) {
// reset state visuals
step(); // Hack to refresh UI labels, but will advance 1 step.
// Better: just reset.
btnReset.click();
}
});
btnStep.addEventListener('click', () => {
state.isRunning = false;
btnRun.innerText = "Run";
cancelAnimationFrame(state.animId);
step();
drawAll();
});
btnRun.addEventListener('click', () => {
state.isRunning = !state.isRunning;
if (state.isRunning) {
btnRun.innerText = "Stop";
loop();
} else {
btnRun.innerText = "Run";
cancelAnimationFrame(state.animId);
}
});
btnReset.addEventListener('click', () => {
state.isRunning = false;
btnRun.innerText = "Run";
cancelAnimationFrame(state.animId);
state.t = 0;
state.x = 1.0;
state.v = 0;
state.a = 0;
state.history = [];
// Initial Labels
m_t.innerText = "0.00";
m_x.innerText = "1.00";
m_v.innerText = "0.00";
m_x_calc.innerText = "...";
m_v_calc.innerText = "...";
m_rk_x.innerText = "...";
m_rk_v.innerText = "...";
drawAll();
});
sliderDt.addEventListener('input', (e) => {
state.dt = parseFloat(e.target.value);
lblDt.innerText = state.dt.toFixed(2);
});
sliderK.addEventListener('input', (e) => {
state.k = parseFloat(e.target.value);
lblK.innerText = state.k.toFixed(1);
});
// Init
setTimeout(() => {
resizeCanvases();
btnReset.click();
}, 100);
})();
</script>
</div>
```
## Basic examples
### Projectile motion
Equations of motion for a particle in free fall are
$$
\begin{align*}
\frac{d^2 x(t)}{dt^2} &= 0 \\
\frac{d^2 y(t)}{dt^2} &= -g
\end{align*}
$$
where $g$ is the acceleration due to gravity. The acceleration of the particle in the $x$ direction is zero. The acceleration of the particle in the $y$ direction is equal to the acceleration due to gravity. The force acting on the particle is the force of gravity.
#### Analytical solution
To find the position of the particle as a function of time, we integrate the equations of motion with respect to time and apply the initial conditions.
**1. Horizontal motion ($x$-direction)**
First, we integrate the acceleration $\frac{d^2 x}{dt^2} = 0$ to find the velocity:
$$
v_x(t) = \int 0 \, dt = C_1
$$
At time $t=0$, the initial velocity is $v_{0x}$. Substituting this into the equation gives $C_1 = v_{0x}$. Thus:
$$
v_x(t) = v_{0x}
$$
Next, we integrate the velocity to find the position:
$$
x(t) = \int v_{0x} \, dt = v_{0x}t + C_2
$$
At time $t=0$, the initial position is $x_0$. Substituting this gives $C_2 = x_0$. The final equation for horizontal position is:
$$
x(t) = v_{0x} t + x_0
$$
**2. Vertical motion ($y$-direction)**
First, we integrate the acceleration $\frac{d^2 y}{dt^2} = -g$ to find the velocity:
$$
v_y(t) = \int -g \, dt = -gt + C_3
$$
At time $t=0$, the initial velocity is $v_{0y}$. Substituting this gives $C_3 = v_{0y}$. Thus:
$$
v_y(t) = -gt + v_{0y}
$$
Next, we integrate the velocity to find the position:
$$
y(t) = \int (-gt + v_{0y}) \, dt = -\frac{1}{2}gt^2 + v_{0y}t + C_4
$$
At time $t=0$, the initial position is $y_0$. Substituting this gives $C_4 = y_0$. The final equation for vertical position is:
$$
y(t) = -\frac{1}{2} g t^2 + v_{0y} t + y_0
$$
**Summary**
The complete analytical solution describing the trajectory of the particle is:
$$
\begin{align*}
x(t) &= v_{0x} t + x_0 \\
y(t) &= -\frac{1}{2} g t^2 + v_{0y} t + y_0
\end{align*}
$$
#### Numerical solution
```{python}
import numpy as np
import matplotlib.pyplot as plt
# Constants
F = (0, -9.81) # Force of gravity (N)
m = 1 # Mass of the particle (kg)
x_0, y_0 = 0, 100 # Initial position (m)
v_x0, v_y0 = 1, 0 # Initial velocity (m/s)
def simulate_trajectory(F, m, x_0, y_0, v_x0, v_y0, t_max, steps):
# Time discretization
t = np.linspace(0, t_max, steps)
h = t[1] - t[0] # Time step
# Initialize position and velocity
x = [x_0]
y = [y_0]
v_x, v_y = v_x0, v_y0
# Euler integration
for i in range(1, len(t)):
a_x, a_y = F[0] / m, F[1] / m # Acceleration
v_x += a_x * h # Update velocity
v_y += a_y * h
x_next = x[-1] + v_x * h # Update position
y_next = y[-1] + v_y * h
# Stop if the particle hits the ground
if y_next < 0:
break
x.append(x_next)
y.append(y_next)
return x, y, t[:len(x)] # Return trajectory and corresponding time
# Simulate the trajectory
x, y, t = simulate_trajectory(F, m, x_0, y_0, v_x0, v_y0, t_max=5, steps=100)
# Create grid for vector field
X, Y = np.meshgrid(np.linspace(-1, 6, 10), np.linspace(0, 120, 5))
U = np.zeros_like(X) # Horizontal component of g is 0
V = -9.81 * np.ones_like(Y) # Vertical component of g is constant
# Time to display the ball and force
t_display = 2.0 # Time at which to show the ball and force
idx = np.argmin(np.abs(t - t_display)) # Find the closest index for the given time
# Visualization
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(x, y, label="Trajectory")
ax.axhline(0, color='green', linestyle='dashed', label='Ground level')
ax.scatter(x_0, y_0, color='red', label='Initial position')
# Add vector field
ax.quiver(X, Y, U, V, color='blue', alpha=0.3, scale=200
, width=0.002,label='Gravitational field')
# Add the ball at the selected time
ball_x, ball_y = x[idx], y[idx]
ax.scatter(ball_x, ball_y, color='orange', s=100, label='Ball (t=2s)')
# Add the force vector acting on the ball
force_x, force_y = F[0] / m, F[1] / m # Force per unit mass
ax.quiver(ball_x, ball_y, force_x, force_y, color='red', angles='xy', scale_units='xy', scale=0.5, label='Force on Ball')
# Labels and title
ax.set_xlabel('x (m)')
ax.set_ylabel('y (m)')
ax.set_title('Particle Trajectory with Gravitational Field and Force on Ball')
ax.legend()
plt.show()
```
```{=html}
<div class="projectile-motion-widget">
<style>
.projectile-motion-widget {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
padding: 20px;
background-color: #fff;
border: 1px solid #e0e0e0;
border-radius: 8px;
color: #333;
display: flex;
flex-direction: column;
gap: 15px;
width: 100%;
max-width: 900px;
margin: 20px auto;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
box-sizing: border-box;
user-select: none;
}
.projectile-motion-widget h3 {
margin: 0;
color: #2c3e50;
font-size: 1.2em;
text-align: center;
text-transform: uppercase;
letter-spacing: 1px;
}
/* Canvas */
.projectile-motion-widget .canvas-container {
position: relative;
background: #fdfdfd;
border: 1px solid #ccc;
border-radius: 4px;
height: 400px;
overflow: hidden;
width: 100%;
}
.projectile-motion-widget canvas {
display: block;
width: 100%;
height: 100%;
}
/* Info Overlay */
.projectile-motion-widget .info-panel {
position: absolute;
top: 10px;
right: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 10px 15px;
border-radius: 4px;
border: 1px solid #ddd;
font-family: 'Courier New', monospace;
font-size: 0.9em;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
pointer-events: none;
}
.projectile-motion-widget .info-row {
display: flex;
justify-content: space-between;
gap: 15px;
margin-bottom: 4px;
}
.projectile-motion-widget .info-label { font-weight: bold; color: #555; }
.projectile-motion-widget .info-val { color: #1565c0; font-weight: bold; }
/* Controls */
.projectile-motion-widget .controls-panel {
background: #f5f5f5;
padding: 15px;
border-radius: 6px;
border: 1px solid #eee;
display: flex;
flex-wrap: wrap;
gap: 20px;
justify-content: center;
align-items: center;
}
.projectile-motion-widget .control-group {
display: flex;
flex-direction: column;
gap: 5px;
min-width: 140px;
}
.projectile-motion-widget label {
font-size: 0.85em;
font-weight: 600;
color: #444;
display: flex;
justify-content: space-between;
}
.projectile-motion-widget input[type=range] {
cursor: pointer;
width: 100%;
}
.projectile-motion-widget button {
padding: 10px 24px;
border: none;
border-radius: 4px;
font-weight: bold;
font-size: 14px;
color: white;
cursor: pointer;
transition: transform 0.1s, background 0.2s;
min-width: 100px;
}
.projectile-motion-widget .btn-fire { background-color: #d32f2f; }
.projectile-motion-widget .btn-fire:hover { background-color: #b71c1c; transform: scale(1.05); }
.projectile-motion-widget .btn-reset { background-color: #607d8b; }
.projectile-motion-widget .btn-reset:hover { background-color: #455a64; transform: scale(1.05); }
.projectile-motion-widget .value-span {
color: #1976d2;
}
/* Legend for vectors */
.projectile-motion-widget .vector-legend {
position: absolute;
bottom: 10px;
right: 10px;
background: rgba(255,255,255,0.8);
padding: 5px 10px;
border-radius: 4px;
font-size: 0.8em;
color: #444;
display: flex;
gap: 15px;
}
.projectile-motion-widget .v-leg-item { display: flex; align-items: center; gap: 5px; }
.projectile-motion-widget .v-dot { width: 10px; height: 3px; }
@media (max-width: 700px) {
.projectile-motion-widget .controls-panel {
flex-direction: column;
align-items: stretch;
}
}
</style>
<h3>Projectile Motion (Analytical Solution)</h3>
<div class="canvas-container">
<canvas class="proj-canvas"></canvas>
<div class="info-panel">
<div class="info-row"><span class="info-label">Time (t):</span> <span class="info-val" id="disp-t">0.00 s</span></div>
<div class="info-row"><span class="info-label">Pos X:</span> <span class="info-val" id="disp-x">0.00 m</span></div>
<div class="info-row"><span class="info-label">Pos Y:</span> <span class="info-val" id="disp-y">0.00 m</span></div>
<div style="margin-top:5px; border-top:1px dashed #ccc; padding-top:5px;"></div>
<div class="info-row"><span class="info-label">Max Height:</span> <span class="info-val" id="disp-hmax">-</span></div>
<div class="info-row"><span class="info-label">Range:</span> <span class="info-val" id="disp-range">-</span></div>
</div>
<div class="vector-legend">
<div class="v-leg-item"><div class="v-dot" style="background:#2e7d32"></div>Velocity</div>
<div class="v-leg-item"><div class="v-dot" style="background:#ef6c00"></div>Acceleration</div>
</div>
</div>
<div class="controls-panel">
<div class="control-group">
<label>Initial Height (y₀): <span class="value-span" id="val-y0">0 m</span></label>
<input type="range" id="slider-y0" min="0" max="100" step="1" value="0">
</div>
<div class="control-group">
<label>Initial Speed (v₀): <span class="value-span" id="val-v0">50 m/s</span></label>
<input type="range" id="slider-v0" min="1" max="100" step="1" value="50">
</div>
<div class="control-group">
<label>Angle (θ): <span class="value-span" id="val-angle">45°</span></label>
<input type="range" id="slider-angle" min="0" max="90" step="1" value="45">
</div>
<div class="control-group">
<label>Gravity (g): <span class="value-span" id="val-g">9.81 m/s²</span></label>
<input type="range" id="slider-g" min="1.6" max="20" step="0.1" value="9.8">
</div>
<div style="display: flex; gap: 10px; justify-content: center;">
<button class="btn-fire">FIRE</button>
<button class="btn-reset">Reset</button>
</div>
</div>
<script>
(function() {
const widget = document.currentScript.parentElement;
const canvas = widget.querySelector('.proj-canvas');
const ctx = canvas.getContext('2d');
// UI References
const sliderY0 = widget.querySelector('#slider-y0');
const sliderV0 = widget.querySelector('#slider-v0');
const sliderAngle = widget.querySelector('#slider-angle');
const sliderG = widget.querySelector('#slider-g');
const valY0 = widget.querySelector('#val-y0');
const valV0 = widget.querySelector('#val-v0');
const valAngle = widget.querySelector('#val-angle');
const valG = widget.querySelector('#val-g');
const dispT = widget.querySelector('#disp-t');
const dispX = widget.querySelector('#disp-x');
const dispY = widget.querySelector('#disp-y');
const dispHmax = widget.querySelector('#disp-hmax');
const dispRange = widget.querySelector('#disp-range');
const btnFire = widget.querySelector('.btn-fire');
const btnReset = widget.querySelector('.btn-reset');
// State
let state = {
y0: 0,
v0: 50,
angle: 45,
g: 9.81,
t: 0,
isRunning: false,
path: [],
animId: null,
// Simulation Parameters
scale: 3.5, // pixels per meter
simSpeed: 4.0 // time multiplier for visualization
};
function resizeCanvas() {
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
draw();
}
window.addEventListener('resize', resizeCanvas);
// --- Physics (Analytical Solution) ---
function getPositionAt(t) {
const rad = state.angle * Math.PI / 180;
const v0x = state.v0 * Math.cos(rad);
const v0y = state.v0 * Math.sin(rad);
// Analytical formulas
const x = v0x * t;
const y = state.y0 + (v0y * t) - (0.5 * state.g * t * t);
// Velocities
const vx = v0x;
const vy = v0y - state.g * t;
return { x, y, vx, vy };
}
function calculateStats() {
const rad = state.angle * Math.PI / 180;
const v0y = state.v0 * Math.sin(rad);
const v0x = state.v0 * Math.cos(rad);
// Max Height
const t_apex = v0y / state.g;
const h_max = state.y0 + (v0y * t_apex) - (0.5 * state.g * t_apex * t_apex);
// Range (Time of flight)
const delta = Math.sqrt(v0y*v0y + 2*state.g*state.y0);
const t_flight = (v0y + delta) / state.g;
const range = v0x * t_flight;
return { h_max, range, t_flight };
}
// --- Drawing ---
function toScreen(x, y) {
return {
x: 50 + x * state.scale,
y: canvas.height - 50 - y * state.scale
};
}
function drawArrow(x, y, vx, vy, color) {
const endX = x + vx;
const endY = y - vy; // Screen Y is flipped relative to vector
const dx = endX - x;
const dy = endY - y;
const angle = Math.atan2(dy, dx);
const head = 8;
ctx.save();
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(endX, endY); ctx.stroke();
ctx.beginPath();
ctx.moveTo(endX, endY);
ctx.lineTo(endX - head*Math.cos(angle-Math.PI/6), endY - head*Math.sin(angle-Math.PI/6));
ctx.lineTo(endX - head*Math.cos(angle+Math.PI/6), endY - head*Math.sin(angle+Math.PI/6));
ctx.fill();
ctx.restore();
}
function drawGridAndField() {
const w = canvas.width;
const h = canvas.height;
const groundY = h - 50;
ctx.clearRect(0, 0, w, h);
// --- Gravitational Vector Field ---
const gridStep = 50;
ctx.save();
ctx.strokeStyle = '#e0e0e0';
ctx.fillStyle = '#e0e0e0';
for (let x = 20; x < w; x += gridStep) {
for (let y = 20; y < groundY; y += gridStep) {
// Draw small downward arrow
const arrowLen = 15;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x, y + arrowLen);
ctx.stroke();
// Arrowhead
ctx.beginPath();
ctx.moveTo(x, y + arrowLen);
ctx.lineTo(x - 3, y + arrowLen - 4);
ctx.lineTo(x + 3, y + arrowLen - 4);
ctx.fill();
}
}
ctx.restore();
// --- Ground ---
ctx.fillStyle = '#f5f5f5';
ctx.fillRect(0, groundY, w, 50);
ctx.strokeStyle = '#999';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, groundY);
ctx.lineTo(w, groundY);
ctx.stroke();
// --- Axes & Labels ---
ctx.strokeStyle = '#ccc';
ctx.lineWidth = 1;
ctx.font = '11px Arial';
ctx.fillStyle = '#555';
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
// Y Axis
ctx.beginPath();
ctx.moveTo(50, 0);
ctx.lineTo(50, h);
ctx.stroke();
// Y Labels (every 20m)
const yStepM = 20;
const maxM = (h - 50) / state.scale;
for(let ym = 0; ym <= maxM; ym += yStepM) {
const sy = groundY - ym * state.scale;
ctx.beginPath();
ctx.moveTo(45, sy);
ctx.lineTo(50, sy);
ctx.stroke();
ctx.fillText(ym + "m", 42, sy);
}
// X Labels (every 20m)
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
const xStepM = 20;
const maxXM = (w - 50) / state.scale;
for(let xm = 0; xm <= maxXM; xm += xStepM) {
const sx = 50 + xm * state.scale;
ctx.beginPath();
ctx.moveTo(sx, groundY);
ctx.lineTo(sx, groundY + 5);
ctx.stroke();
if (xm > 0) ctx.fillText(xm + "m", sx, groundY + 8);
}
// Origin
ctx.fillText("0", 50, groundY + 8);
}
function drawGhostPath() {
// Predict path
const stats = calculateStats();
const totalT = stats.t_flight;
ctx.beginPath();
ctx.strokeStyle = '#bbb';
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
const steps = 60;
for(let i=0; i<=steps; i++) {
const t = (i/steps) * totalT;
const pos = getPositionAt(t);
const s = toScreen(pos.x, pos.y);
if(i===0) ctx.moveTo(s.x, s.y);
else ctx.lineTo(s.x, s.y);
}
ctx.stroke();
ctx.setLineDash([]);
}
function draw() {
drawGridAndField();
// Draw trajectory so far
if (state.path.length > 0) {
ctx.beginPath();
ctx.strokeStyle = '#1976d2';
ctx.lineWidth = 3;
const start = toScreen(state.path[0].x, state.path[0].y);
ctx.moveTo(start.x, start.y);
for (let p of state.path) {
const s = toScreen(p.x, p.y);
ctx.lineTo(s.x, s.y);
}
ctx.stroke();
} else if (!state.isRunning) {
// If reset, show ghost path
drawGhostPath();
}
// Draw Object
const pos = getPositionAt(state.t);
const scr = toScreen(pos.x, pos.y);
// Cannon/Start Point
const startScr = toScreen(0, state.y0);
ctx.fillStyle = '#555';
ctx.fillRect(startScr.x - 10, startScr.y, 20, canvas.height - startScr.y - 50);
// Ball
ctx.beginPath();
ctx.fillStyle = '#d32f2f';
ctx.arc(scr.x, scr.y, 8, 0, Math.PI*2);
ctx.fill();
// Vectors
if (pos.y >= 0) {
// Velocity (Green)
const vScale = 1.0;
drawArrow(scr.x, scr.y, pos.vx * vScale, pos.vy * vScale, '#2e7d32');
// Acceleration (Orange) [0, -g]
// Scale up g for visibility
const aScale = 4.0;
drawArrow(scr.x, scr.y, 0, -state.g * aScale, '#ef6c00');
}
}
function updateLoop() {
if (!state.isRunning) return;
// Update Time (dt = 16ms approx * speed)
const dt = 0.016 * state.simSpeed;
state.t += dt;
// Get Pos
const pos = getPositionAt(state.t);
// Record path
state.path.push(pos);
// Check collision
if (pos.y < 0) {
state.isRunning = false;
// Clamp to ground
const stats = calculateStats();
state.t = stats.t_flight;
const finalPos = getPositionAt(state.t);
state.path.push(finalPos);
updateInfo(finalPos);
draw();
return;
}
updateInfo(pos);
draw();
state.animId = requestAnimationFrame(updateLoop);
}
function updateInfo(pos) {
dispT.innerText = state.t.toFixed(2) + " s";
dispX.innerText = pos.x.toFixed(2) + " m";
dispY.innerText = Math.max(0, pos.y).toFixed(2) + " m";
}
// --- Events ---
function updateStatsDisplay() {
const stats = calculateStats();
dispHmax.innerText = stats.h_max.toFixed(2) + " m";
dispRange.innerText = stats.range.toFixed(2) + " m";
}
sliderY0.addEventListener('input', (e) => {
state.y0 = parseFloat(e.target.value);
valY0.innerText = state.y0 + " m";
resetSim();
});
sliderV0.addEventListener('input', (e) => {
state.v0 = parseFloat(e.target.value);
valV0.innerText = state.v0 + " m/s";
resetSim();
});
sliderAngle.addEventListener('input', (e) => {
state.angle = parseFloat(e.target.value);
valAngle.innerText = state.angle + "°";
resetSim();
});
sliderG.addEventListener('input', (e) => {
state.g = parseFloat(e.target.value);
valG.innerText = state.g + " m/s²";
resetSim();
});
btnFire.addEventListener('click', () => {
if (state.isRunning) return;
resetSim(false); // don't full reset, just prep
state.isRunning = true;
updateLoop();
});
btnReset.addEventListener('click', () => {
resetSim();
});
function resetSim(fullReset = true) {
state.isRunning = false;
cancelAnimationFrame(state.animId);
if (fullReset) {
state.t = 0;
state.path = [];
updateInfo({x: 0, y: state.y0});
} else {
state.t = 0;
state.path = [];
}
updateStatsDisplay();
draw();
}
// Init
setTimeout(() => {
resizeCanvas();
updateStatsDisplay();
resetSim();
}, 100);
})();
</script>
</div>
```
### Harmonic oscillation
Let $\mathbb{F}=-k x$ be the force acting on the particle. The equation of motion is
$$
\frac{d^2 x(t)}{dt^2} = -\frac{k}{m} x(t)
$$
where $k$ is the spring constant and $m$ is the mass of the particle. The acceleration of the particle is proportional to its position, and the force acting on the particle is the force of a spring, equal to the spring constant times the position of the particle.
```{python}
import numpy as np
import matplotlib.pyplot as plt
# Updated constant
k = 0.5 # Updated spring constant
displacements = [1, 2, 3, 4, 5] # Displacements (x)
forces = [-k * x for x in displacements] # Corresponding forces (F)
# Positions for visualization
y_positions = np.linspace(1, len(displacements) + 1, len(displacements)) # Avoid overlapping
# Visualization
fig, ax = plt.subplots(figsize=(8, 6))
# Draw force vectors for each displacement
for x, F, y in zip(displacements, forces, y_positions):
# Draw ball position
ax.scatter(x, y, color='orange', s=100, label=f"x = {x}, F = {F:.2f}" if y == y_positions[0] else "")
# Draw force vector
ax.quiver(x, y, F, 0, angles='xy', scale_units='xy', scale=1, color='blue', width=0.005)
# Add annotation for calculations
ax.text(x + F / 2, y + 0.2, f"F = {-k:.1f}*{x} = {F:.2f}", fontsize=9, color='black', alpha=.8)
# Labels and title
ax.axhline(0, color='black', linewidth=0.5, linestyle='dashed') # Equilibrium line
ax.set_xlim(0, 6)
ax.set_ylim(0, len(displacements) + 2)
ax.set_xlabel("Displacement $x$")
ax.set_ylabel("Vertical position (for clarity)")
ax.set_title("Force Linearly Dependent on Displacement with Annotations")
plt.grid(True, linestyle='--', alpha=0.6)
plt.show()
```
#### Analytical solution
The analytical solution of the equation of motion for a particle in simple harmonic motion is
$$
x(t) = A \sin(\omega t) + B \cos(\omega t)
$$
where $A$ and $B$ are the amplitudes of the particle, and $\omega$ is the angular frequency of the particle
$$
\omega = \sqrt{\frac{k}{m}}
$$
The angular frequency is equal to the square root of the spring constant divided by the mass of the particle. The phase angle $\phi$ determines the initial phase of the particle.
#### Geogebra example
::: {.geogebra-instruction}
* t=Slider[1, 100, 0.1]
* P=Point[0, sin(t)]
:::
#### Numerical solution
```{python}
import numpy as np
import matplotlib.pyplot as plt
# Parameters
k = 1 # Spring constant
m = 1 # Mass of the particle
x_0 = 1 # Initial position
v_0 = 0 # Initial velocity
# Derived parameters
omega = np.sqrt(k / m) # Angular frequency
# Time array
t = np.linspace(0, 50, 500)
dt = t[1] - t[0]
# Numerical solution (Euler method)
x_num = [x_0]
v = v_0
for i in range(1, len(t)):
a = -k / m * x_num[-1] # Acceleration
v += a * dt # Update velocity
x_num.append(x_num[-1] + v * dt) # Update position
# Analytical solution
A = x_0
B = v_0 / omega
x_analytical = B * np.sin(omega * t) + A * np.cos(omega * t)
# Plotting the position vs time
plt.figure(figsize=(8, 4))
plt.plot(t, x_num, label="Numerical Solution", linestyle='--')
plt.plot(t, x_analytical, label="Analytical Solution", linestyle='-')
plt.xlabel('Time (t)')
plt.ylabel('Position (x)')
plt.title('Harmonic Motion in One Dimension')
plt.legend()
plt.grid()
plt.show()
```
```{=html}
<div class="harmonic-oscillator-widget">
<style>
.harmonic-oscillator-widget {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
padding: 20px;
background-color: #fff;
border: 1px solid #e0e0e0;
border-radius: 8px;
color: #333;
display: flex;
flex-direction: column;
gap: 15px;
width: 100%;
max-width: 900px;
margin: 20px auto;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
box-sizing: border-box;
user-select: none;
}
.harmonic-oscillator-widget h3 {
margin: 0;
color: #2c3e50;
font-size: 1.2em;
text-align: center;
text-transform: uppercase;
letter-spacing: 1px;
}
/* Sekcja Symulacji */
.harmonic-oscillator-widget .sim-container {
position: relative;
background: #f9f9f9;
border: 1px solid #ccc;
border-radius: 4px;
height: 220px;
overflow: hidden;
}
.harmonic-oscillator-widget canvas {
display: block;
width: 100%;
height: 100%;
}
.harmonic-oscillator-widget .sim-legend {
position: absolute;
top: 10px;
right: 10px;
background: rgba(255,255,255,0.8);
padding: 5px 10px;
border-radius: 4px;
font-size: 0.85em;
color: #444;
display: flex;
gap: 15px;
border: 1px solid #ddd;
}
.harmonic-oscillator-widget .leg-item { display: flex; align-items: center; gap: 6px; }
.harmonic-oscillator-widget .line { width: 20px; height: 3px; }
/* Sekcja Wykresów */
.harmonic-oscillator-widget .charts-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
height: 220px;
}
.harmonic-oscillator-widget .chart-box {
border: 1px solid #ddd;
background: #fff;
border-radius: 4px;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
.harmonic-oscillator-widget .chart-title {
font-size: 0.8em;
font-weight: bold;
text-align: center;
padding: 5px;
background: #f0f0f0;
border-bottom: 1px solid #ddd;
z-index: 10;
}
/* Current Value Overlay on Charts */
.harmonic-oscillator-widget .chart-val {
position: absolute;
top: 30px;
right: 10px;
font-family: monospace;
font-weight: bold;
background: rgba(255,255,255,0.8);
padding: 2px 5px;
border-radius: 3px;
}
/* Panel Sterowania */
.harmonic-oscillator-widget .controls {
background: #f5f5f5;
padding: 15px;
border-radius: 6px;
border: 1px solid #eee;
display: flex;
flex-wrap: wrap;
gap: 20px;
justify-content: center;
align-items: center;
}
.harmonic-oscillator-widget .control-group {
display: flex;
flex-direction: column;
gap: 5px;
min-width: 140px;
}
.harmonic-oscillator-widget label {
font-size: 0.85em;
font-weight: 600;
color: #555;
display: flex;
justify-content: space-between;
}
.harmonic-oscillator-widget input[type=range] {
cursor: pointer;
width: 100%;
}
.harmonic-oscillator-widget button {
padding: 10px 24px;
border: none;
border-radius: 4px;
font-weight: bold;
font-size: 14px;
color: white;
cursor: pointer;
transition: transform 0.1s, background 0.2s;
}
.harmonic-oscillator-widget .btn-start { background-color: #2e7d32; }
.harmonic-oscillator-widget .btn-start:hover { background-color: #1b5e20; transform: scale(1.05); }
.harmonic-oscillator-widget .btn-reset { background-color: #607d8b; }
.harmonic-oscillator-widget .btn-reset:hover { background-color: #455a64; transform: scale(1.05); }
.harmonic-oscillator-widget .val-display { color: #1565c0; }
@media (max-width: 700px) {
.harmonic-oscillator-widget .charts-container {
grid-template-columns: 1fr;
height: auto;
}
.harmonic-oscillator-widget .chart-box {
height: 180px;
}
}
</style>
<h3>Harmonic Oscillator (Spring-Mass System)</h3>
<!-- Symulacja -->
<div class="sim-container">
<canvas class="sim-canvas"></canvas>
<div class="sim-legend">
<div class="leg-item"><div class="line" style="background:#2e7d32"></div> Velocity (v)</div>
<div class="leg-item"><div class="line" style="background:#ef6c00"></div> Acceleration (a)</div>
</div>
</div>
<!-- Wykresy -->
<div class="charts-container">
<div class="chart-box">
<div class="chart-title" style="color:#1976d2">Position x(t) [m]</div>
<div class="chart-val" id="val-x-live" style="color:#1976d2">0.00</div>
<canvas class="chart-x"></canvas>
</div>
<div class="chart-box">
<div class="chart-title" style="color:#2e7d32">Velocity v(t) [m/s]</div>
<div class="chart-val" id="val-v-live" style="color:#2e7d32">0.00</div>
<canvas class="chart-v"></canvas>
</div>
</div>
<!-- Sterowanie -->
<div class="controls">
<div class="control-group">
<label>Spring Constant (k): <span class="val-display" id="val-k">2.0 N/m</span></label>
<input type="range" id="slider-k" min="0.5" max="10.0" step="0.1" value="2.0">
</div>
<div class="control-group">
<label>Mass (m): <span class="val-display" id="val-m">1.0 kg</span></label>
<input type="range" id="slider-m" min="0.1" max="5.0" step="0.1" value="1.0">
</div>
<div class="control-group">
<label>Anim Speed: <span class="val-display" id="val-speed">1.0x</span></label>
<input type="range" id="slider-speed" min="0.1" max="3.0" step="0.1" value="1.0">
</div>
<div style="display: flex; gap: 10px;">
<button class="btn-start">Start / Stop</button>
<button class="btn-reset">Reset</button>
</div>
</div>
<script>
(function() {
const widget = document.currentScript.parentElement;
// Canvas refs
const simCanvas = widget.querySelector('.sim-canvas');
const chartXCanvas = widget.querySelector('.chart-x');
const chartVCanvas = widget.querySelector('.chart-v');
const ctxSim = simCanvas.getContext('2d');
const ctxX = chartXCanvas.getContext('2d');
const ctxV = chartVCanvas.getContext('2d');
// UI refs
const sliderK = widget.querySelector('#slider-k');
const sliderM = widget.querySelector('#slider-m');
const sliderSpeed = widget.querySelector('#slider-speed');
const valK = widget.querySelector('#val-k');
const valM = widget.querySelector('#val-m');
const valSpeed = widget.querySelector('#val-speed');
const valXLive = widget.querySelector('#val-x-live');
const valVLive = widget.querySelector('#val-v-live');
const btnStart = widget.querySelector('.btn-start');
const btnReset = widget.querySelector('.btn-reset');
// State
let state = {
t: 0,
x: 3.0,
v: 0,
a: 0,
k: 2.0,
m: 1.0,
dt: 0.016, // approx 60fps step base
timeScale: 1.0,
history: [],
isRunning: false,
animId: null
};
function resizeCanvases() {
[simCanvas, chartXCanvas, chartVCanvas].forEach(c => {
c.width = c.clientWidth;
c.height = c.clientHeight;
});
drawAll();
}
window.addEventListener('resize', resizeCanvases);
// --- Physics Engine ---
function updatePhysics() {
// Apply time scaling
const effectiveDt = state.dt * state.timeScale;
// Symplectic Euler
state.a = -(state.k / state.m) * state.x;
state.v += state.a * effectiveDt;
state.x += state.v * effectiveDt;
state.t += effectiveDt;
state.history.push({ t: state.t, x: state.x, v: state.v });
// Maintain history buffer (approx 5-6 seconds of data)
// If dt is small, history grows fast.
// Let's keep fixed number of points for graph width
if (state.history.length > 400) state.history.shift();
}
// --- Drawing ---
function drawArrow(ctx, x, y, len, color) {
if (Math.abs(len) < 5) return;
const dir = len > 0 ? 1 : -1;
const endX = x + len;
ctx.save();
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(endX, y);
ctx.stroke();
// Head
const head = 8;
ctx.beginPath();
ctx.moveTo(endX, y);
ctx.lineTo(endX - head*dir, y - 5);
ctx.lineTo(endX - head*dir, y + 5);
ctx.fill();
ctx.restore();
}
function drawSimulation() {
const w = simCanvas.width;
const h = simCanvas.height;
const cx = w / 2;
const cy = h / 2 - 20;
const scale = 40; // px per meter (zoomed out a bit)
ctxSim.clearRect(0, 0, w, h);
// Floor / Ruler
const floorY = cy + 40;
ctxSim.fillStyle = '#f0f0f0';
ctxSim.fillRect(0, floorY, w, h-floorY);
ctxSim.strokeStyle = '#999';
ctxSim.beginPath(); ctxSim.moveTo(0, floorY); ctxSim.lineTo(w, floorY); ctxSim.stroke();
// Ruler Ticks
ctxSim.fillStyle = '#666';
ctxSim.font = '10px monospace';
ctxSim.textAlign = 'center';
for (let i = -8; i <= 8; i++) {
const tx = cx + i * scale;
if (tx < 0 || tx > w) continue;
ctxSim.beginPath(); ctxSim.moveTo(tx, floorY); ctxSim.lineTo(tx, floorY + 8); ctxSim.stroke();
ctxSim.fillText(i, tx, floorY + 20);
}
// Equilibrium Line
ctxSim.strokeStyle = '#1976d2';
ctxSim.lineWidth = 1;
ctxSim.setLineDash([5, 3]);
ctxSim.beginPath(); ctxSim.moveTo(cx, 20); ctxSim.lineTo(cx, floorY); ctxSim.stroke();
ctxSim.setLineDash([]);
ctxSim.fillStyle = '#1976d2';
ctxSim.fillText("0", cx, 15);
// Mass position
const massX = cx + state.x * scale;
const massW = 50;
const massH = 50;
// Spring
ctxSim.strokeStyle = '#444';
ctxSim.lineWidth = 2;
ctxSim.beginPath();
ctxSim.moveTo(0, cy);
const endSpringX = massX - massW/2;
const coils = 16;
const coilW = (endSpringX) / coils;
for(let i=0; i<coils; i++) {
const x = i * coilW;
ctxSim.lineTo(x + coilW/2, cy + (i%2===0 ? 12 : -12));
ctxSim.lineTo(x + coilW, cy);
}
ctxSim.lineTo(endSpringX, cy);
ctxSim.stroke();
// Mass
ctxSim.fillStyle = '#37474f';
ctxSim.fillRect(massX - massW/2, cy - massH/2, massW, massH);
ctxSim.strokeStyle = '#263238';
ctxSim.strokeRect(massX - massW/2, cy - massH/2, massW, massH);
ctxSim.fillStyle = 'white';
ctxSim.font = 'bold 16px Arial';
ctxSim.fillText("M", massX, cy);
// VECTORS (Scaled for visibility)
drawArrow(ctxSim, massX, cy + 35, state.v * 10, '#2e7d32');
drawArrow(ctxSim, massX, cy + 50, state.a * 10, '#ef6c00');
}
function drawAxes(ctx, w, h, min, max) {
ctx.clearRect(0, 0, w, h);
const padLeft = 40;
const padBottom = 20;
const graphH = h - padBottom;
// Grid
ctx.strokeStyle = '#eee';
ctx.lineWidth = 1;
const zeroY = graphH - ((0 - min) / (max - min)) * graphH;
// Zero Line
if (zeroY >= 0 && zeroY <= graphH) {
ctx.strokeStyle = '#bbb';
ctx.beginPath(); ctx.moveTo(padLeft, zeroY); ctx.lineTo(w, zeroY); ctx.stroke();
}
// Labels Y
ctx.fillStyle = '#666';
ctx.font = '10px monospace';
ctx.textAlign = 'right';
ctx.fillText(max.toFixed(1), padLeft - 5, 10);
if (Math.abs(min) > 0.1 && Math.abs(max) > 0.1) ctx.fillText("0.0", padLeft - 5, zeroY + 3);
ctx.fillText(min.toFixed(1), padLeft - 5, graphH);
// Axes lines
ctx.strokeStyle = '#888';
ctx.beginPath();
ctx.moveTo(padLeft, 0); ctx.lineTo(padLeft, graphH); // Y axis
ctx.moveTo(padLeft, graphH); ctx.lineTo(w, graphH); // X axis
ctx.stroke();
return { x: padLeft, y: 0, w: w - padLeft, h: graphH };
}
function drawGraph(ctx, data, key, color) {
const w = ctx.canvas.width;
const h = ctx.canvas.height;
if (data.length === 0) return;
// Auto-scale
let maxVal = 0.5; // Minimum range
for(let d of data) {
if (Math.abs(d[key]) > maxVal) maxVal = Math.abs(d[key]);
}
maxVal *= 1.2;
const minVal = -maxVal;
const area = drawAxes(ctx, w, h, minVal, maxVal);
if (data.length < 2) return;
// Plot
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.beginPath();
const maxPts = 400;
const stepX = area.w / (maxPts - 1);
// Start drawing from right to left or fix right edge?
// "Current" is at right edge (index length-1).
// We draw the whole buffer.
for (let i = 0; i < data.length; i++) {
const val = data[i][key];
const y = area.h - ((val - minVal) / (maxVal - minVal)) * area.h;
// Align right: The last point should be at w.
// x = area.x + area.w - (data.length - 1 - i) * stepX
const x = area.x + area.w - (data.length - 1 - i) * stepX;
if (x < area.x) continue; // Clip left
if (i === 0 || x < area.x + stepX) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
// Current value MARKER
const last = data[data.length-1];
const ly = area.h - ((last[key] - minVal) / (maxVal - minVal)) * area.h;
const lx = area.x + area.w; // Always at right edge
// Draw vertical guide line
ctx.strokeStyle = 'rgba(0,0,0,0.2)';
ctx.setLineDash([2, 2]);
ctx.beginPath(); ctx.moveTo(lx, 0); ctx.lineTo(lx, area.h); ctx.stroke();
ctx.setLineDash([]);
// Draw Dot
ctx.fillStyle = color;
ctx.beginPath(); ctx.arc(lx, ly, 5, 0, Math.PI*2); ctx.fill();
// Pulse ring
ctx.strokeStyle = color;
ctx.beginPath(); ctx.arc(lx, ly, 8, 0, Math.PI*2); ctx.stroke();
}
function drawAll() {
drawSimulation();
drawGraph(ctxX, state.history, 'x', '#1976d2');
drawGraph(ctxV, state.history, 'v', '#2e7d32');
// Update Live Values
valXLive.innerText = state.x.toFixed(2);
valVLive.innerText = state.v.toFixed(2);
}
function loop() {
if (!state.isRunning) return;
updatePhysics(); // One step per frame for smoothness combined with timeScale
drawAll();
state.animId = requestAnimationFrame(loop);
}
// Events
sliderK.addEventListener('input', (e) => {
state.k = parseFloat(e.target.value);
valK.innerText = state.k.toFixed(1) + " N/m";
if (!state.isRunning) drawAll();
});
sliderM.addEventListener('input', (e) => {
state.m = parseFloat(e.target.value);
valM.innerText = state.m.toFixed(1) + " kg";
if (!state.isRunning) drawAll();
});
sliderSpeed.addEventListener('input', (e) => {
state.timeScale = parseFloat(e.target.value);
valSpeed.innerText = state.timeScale.toFixed(1) + "x";
});
btnStart.addEventListener('click', () => {
state.isRunning = !state.isRunning;
if (state.isRunning) loop();
else cancelAnimationFrame(state.animId);
});
btnReset.addEventListener('click', () => {
state.isRunning = false;
cancelAnimationFrame(state.animId);
state.t = 0;
state.x = 3.0;
state.v = 0;
state.history = [];
drawAll();
});
setTimeout(() => {
resizeCanvases();
drawAll();
}, 100);
})();
</script>
</div>
```
### Pendulum Motion
#### Introduction
The motion of a simple pendulum is a classic example of harmonic motion. A pendulum consists of a mass $m$ (called the bob) attached to a string or rod of length $L$, which is fixed at one end and free to swing back and forth under the influence of gravity. For small angles, the motion can be approximated as simple harmonic motion.
#### Equations of Motion
The equation of motion for a pendulum is derived from Newton's second law. The force acting on the pendulum is the component of the gravitational force tangential to the arc of its swing:
$$
F = -mg \sin(\theta)
$$
```{python}
import numpy as np
import matplotlib.pyplot as plt
# Constants
pivot = (0, 10) # Point of attachment of the pendulum
L = 10 # Length of the pendulum
theta1 = np.radians(30) # First angle of displacement (in radians)
theta2 = np.radians(60) # Second angle of displacement (in radians)
g = 9.81 # Gravitational acceleration
m = 0.5 # Mass of the pendulum bob
# Calculate bob position for both angles
x_bob1 = pivot[0] + L * np.sin(theta1) # x-coordinate for θ=30°
y_bob1 = pivot[1] - L * np.cos(theta1) # y-coordinate for θ=30°
x_bob2 = pivot[0] + L * np.sin(theta2) # x-coordinate for θ=60°
y_bob2 = pivot[1] - L * np.cos(theta2) # y-coordinate for θ=60°
# Calculate forces for both angles
F_gravity = m * g # Magnitude of gravitational force
F_tangential1 = F_gravity * np.sin(theta1) # Tangential force for θ=30°
F_tangential2 = F_gravity * np.sin(theta2) # Tangential force for θ=60°
# Circle parameters for light shading
circle = plt.Circle(pivot, L, color='gray', fill=False, linestyle='dashed', alpha=0.5)
# Visualization setup
fig, ax = plt.subplots(figsize=(8, 8))
# Add circular path for the pendulum (single circle for both cases)
ax.add_artist(circle) # Add the circular path
# Draw pendulum lines for both cases
ax.plot([pivot[0], x_bob1], [pivot[1], y_bob1], color='black', linewidth=1.5, label='Pendulum (θ=30°)')
ax.plot([pivot[0], x_bob2], [pivot[1], y_bob2], color='black', linewidth=1.5, linestyle='dotted', label='Pendulum (θ=60°)')
# Draw bobs as larger circles for both cases
bob1 = plt.Circle((x_bob1, y_bob1), 0.5, color='orange', zorder=5) # Bob for θ=30°
bob2 = plt.Circle((x_bob2, y_bob2), 0.5, color='green', zorder=5) # Bob for θ=60°
ax.add_artist(bob1)
ax.add_artist(bob2)
# Draw force vectors for θ=30°
ax.quiver(x_bob1, y_bob1, 0, -F_gravity, angles='xy', scale_units='xy', scale=1, color='red', label='Gravitational Force (mg)')
ax.quiver(
x_bob1, y_bob1,
-F_tangential1 * np.cos(theta1), -F_tangential1 * np.sin(theta1),
angles='xy', scale_units='xy', scale=1, color='blue', label=r'Tangential Force ($-mg \sin(\theta)$)'
)
# Draw force vectors for θ=60°
ax.quiver(x_bob2, y_bob2, 0, -F_gravity, angles='xy', scale_units='xy', scale=1, color='darkred')
ax.quiver(
x_bob2, y_bob2,
-F_tangential2 * np.cos(theta2), -F_tangential2 * np.sin(theta2),
angles='xy', scale_units='xy', scale=1, color='darkblue', label=r'Tangential Force ($-mg \sin(\theta)$, θ=60°)'
)
# Labels and title
ax.set_xlim(-L - 2, L + 2)
ax.set_ylim(-5, 12)
ax.set_aspect('equal', adjustable='box')
ax.axhline(pivot[1], color='black', linestyle='dashed', linewidth=1.5) # Horizontal reference
ax.set_title("Forces Acting on a Pendulum (Two Positions)")
ax.set_xlabel("Horizontal Position")
ax.set_ylabel("Vertical Position")
ax.legend()
plt.grid(True, linestyle='--', alpha=0.6)
plt.show()
```
Using the relationship $F = ma$ and the angular acceleration $a = L \frac{d^2 \theta}{dt^2}$, we get:
$$
L \frac{d^2 \theta}{dt^2} = -g \sin(\theta)
$$
Dividing through by $L$, the equation becomes:
$$
\frac{d^2 \theta}{dt^2} + \frac{g}{L} \sin(\theta) = 0
$$
For small angles ($\sin(\theta) \approx \theta$), the equation simplifies to:
$$
\frac{d^2 \theta}{dt^2} + \frac{g}{L} \theta = 0
$$
This is the equation for simple harmonic motion with angular frequency:
$$
\omega = \sqrt{\frac{g}{L}}
$$
```{=html}
<!-- PENDULUM FORCES LAB COMPONENT START -->
<div id="force-app-wrapper">
<style>
/* IZOLACJA STYLÓW (SCOPED CSS) */
#force-app-wrapper {
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
background-color: #f8fafc;
color: #334155;
padding: 0;
margin: 0;
box-sizing: border-box;
width: 100%;
/* ZMIANA: height: auto zamiast 100%, aby uniknąć problemów w Quarto */
height: auto;
min-height: 700px;
display: flex;
flex-direction: column;
border: 1px solid #e2e8f0;
border-radius: 8px;
overflow: hidden;
position: relative; /* Dla bezpieczeństwa */
}
#force-app-wrapper * {
box-sizing: border-box;
}
/* LAYOUT */
#force-app-wrapper .app-layout {
display: flex;
flex-direction: column;
/* ZMIANA: height: 100% zastąpione min-height, by wypełniał wrapper */
min-height: 700px;
flex: 1;
}
@media (min-width: 900px) {
#force-app-wrapper .app-layout {
flex-direction: row;
}
}
/* CANVAS AREA */
#force-app-wrapper .canvas-area {
flex: 1;
/* ZMIANA: position relative jest kluczowe dla absolute dziecka */
position: relative;
background-color: #ffffff;
overflow: hidden;
min-height: 400px;
cursor: grab;
order: 2;
}
#force-app-wrapper .canvas-area:active {
cursor: grabbing;
}
#force-app-wrapper canvas {
display: block;
/* ZMIANA: position absolute wyjmuje canvas z przepływu dokumentu -> STOP WZROSTU */
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
/* CONTROLS AREA */
#force-app-wrapper .controls-area {
width: 100%;
background-color: #f1f5f9;
border-right: 1px solid #cbd5e1;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
/* ZMIANA: zamiast overflow na kontenerze, tutaj zarządzamy scrollem */
overflow-y: auto;
max-height: 700px; /* Limit wysokości panelu na mobile */
order: 1;
}
@media (min-width: 900px) {
#force-app-wrapper .controls-area {
width: 320px;
flex-shrink: 0;
height: auto; /* Na desktopie wysokość dopasowana do flexa */
max-height: none;
}
}
/* TYPOGRAPHY & UI ELEMENTS */
#force-app-wrapper h2 {
margin: 0 0 0.5rem 0;
font-size: 1.25rem;
color: #0f172a;
font-weight: 700;
}
#force-app-wrapper .section-title {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #64748b;
font-weight: 700;
margin-bottom: 0.75rem;
border-bottom: 2px solid #e2e8f0;
padding-bottom: 0.25rem;
}
#force-app-wrapper .control-group {
margin-bottom: 1rem;
}
#force-app-wrapper label {
display: flex;
align-items: center;
font-size: 0.9rem;
margin-bottom: 0.5rem;
cursor: pointer;
user-select: none;
}
#force-app-wrapper input[type="checkbox"] {
margin-right: 0.75rem;
width: 1.1rem;
height: 1.1rem;
accent-color: #2563eb;
cursor: pointer;
}
#force-app-wrapper input[type="range"] {
width: 100%;
margin-top: 0.25rem;
accent-color: #475569;
cursor: pointer;
}
#force-app-wrapper .val-display {
font-family: 'Courier New', monospace;
font-weight: bold;
font-size: 0.85rem;
color: #334155;
margin-left: auto;
}
#force-app-wrapper button {
width: 100%;
padding: 0.75rem;
border: none;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
font-size: 0.95rem;
margin-bottom: 0.5rem;
}
#force-app-wrapper .btn-primary {
background-color: #2563eb;
color: white;
}
#force-app-wrapper .btn-primary:hover { background-color: #1d4ed8; }
#force-app-wrapper .btn-secondary {
background-color: #ffffff;
border: 1px solid #cbd5e1;
color: #334155;
}
#force-app-wrapper .btn-secondary:hover { background-color: #f8fafc; border-color: #94a3b8; }
/* LEGEND COLORS */
#force-app-wrapper .color-dot {
width: 12px;
height: 12px;
border-radius: 50%;
display: inline-block;
margin-right: 8px;
}
/* VECTOR INFO OVERLAY */
#force-app-wrapper .overlay-info {
position: absolute;
top: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 8px 12px;
border-radius: 6px;
font-size: 0.8rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
pointer-events: none;
border: 1px solid #e2e8f0;
z-index: 10;
}
/* Specific vector colors used in CSS for legends */
#force-app-wrapper .c-gravity { background-color: #ef4444; } /* Red */
#force-app-wrapper .c-tension { background-color: #854d0e; } /* Brown */
#force-app-wrapper .c-net { background-color: #9333ea; } /* Purple */
#force-app-wrapper .c-velocity { background-color: #16a34a; }/* Green */
#force-app-wrapper .c-accel { background-color: #f97316; } /* Orange */
#force-app-wrapper .c-comp { background-color: #94a3b8; } /* Slate */
</style>
<div class="app-layout">
<!-- Controls Panel (First in code = Left side) -->
<div class="controls-area">
<div>
<h2>Forces Lab</h2>
<p style="font-size: 0.85rem; color: #64748b; margin-top:0;">Dynamic simulation of a simple pendulum.</p>
</div>
<div class="control-group">
<button id="btn-play-pause" class="btn-primary">START</button>
<button id="btn-reset" class="btn-secondary">RESET</button>
</div>
<div>
<div class="section-title">Vectors (Show/Hide)</div>
<label>
<span class="color-dot c-gravity"></span> Gravity Force (mg)
<input type="checkbox" id="chk-gravity" checked style="margin-left: auto; margin-right: 0;">
</label>
<label>
<span class="color-dot c-comp"></span> Gravity Comp. (Tan/Rad)
<input type="checkbox" id="chk-components" style="margin-left: auto; margin-right: 0;">
</label>
<label>
<span class="color-dot c-tension"></span> Tension (T)
<input type="checkbox" id="chk-tension" checked style="margin-left: auto; margin-right: 0;">
</label>
<label>
<span class="color-dot c-net"></span> Net Force (F_net)
<input type="checkbox" id="chk-net" style="margin-left: auto; margin-right: 0;">
</label>
<div style="margin: 0.5rem 0; border-top: 1px dashed #cbd5e1;"></div>
<label>
<span class="color-dot c-velocity"></span> Velocity (v)
<input type="checkbox" id="chk-velocity" style="margin-left: auto; margin-right: 0;">
</label>
<label>
<span class="color-dot c-accel"></span> Acceleration (a)
<input type="checkbox" id="chk-accel" style="margin-left: auto; margin-right: 0;">
</label>
</div>
<div>
<div class="section-title">Parameters</div>
<div class="control-group">
<label>
Simulation Speed
<span id="val-speed" class="val-display">1.0x</span>
</label>
<input type="range" id="rng-sim-speed" min="0.1" max="1.0" step="0.1" value="1.0">
</div>
<div class="control-group">
<label>
String Length
<span id="val-length" class="val-display">2.0m</span>
</label>
<input type="range" id="rng-length" min="1.0" max="4.0" step="0.1" value="2.0">
</div>
<div class="control-group">
<label>
Vector Scale
<span id="val-vscale" class="val-display">80%</span>
</label>
<input type="range" id="rng-vector-scale" min="0.2" max="1.5" step="0.1" value="0.8">
</div>
</div>
</div>
<!-- Visualization Canvas (Second in code = Right side) -->
<div class="canvas-area">
<canvas id="forces-canvas"></canvas>
<div class="overlay-info">
Angle: <span id="info-theta">0</span>°<br>
G-Force: <span id="info-gforce">1.0</span> g
</div>
<!-- Instruction for user -->
<div style="position:absolute; bottom:10px; left:10px; font-size:0.75rem; color:#94a3b8; pointer-events:none;">
Tip: Drag with mouse to set angle.
</div>
</div>
</div>
<script>
(function() {
// --- CONFIGURATION & VARIABLES ---
const wrapper = document.getElementById('force-app-wrapper');
const canvas = document.getElementById('forces-canvas');
const ctx = canvas.getContext('2d');
// UI Elements
const btnPlay = document.getElementById('btn-play-pause');
const btnReset = document.getElementById('btn-reset');
const chkGravity = document.getElementById('chk-gravity');
const chkComponents = document.getElementById('chk-components');
const chkTension = document.getElementById('chk-tension');
const chkNet = document.getElementById('chk-net');
const chkVelocity = document.getElementById('chk-velocity');
const chkAccel = document.getElementById('chk-accel');
const rngSpeed = document.getElementById('rng-sim-speed');
const rngLength = document.getElementById('rng-length');
const rngVScale = document.getElementById('rng-vector-scale');
const valSpeed = document.getElementById('val-speed');
const valLength = document.getElementById('val-length');
const valVScale = document.getElementById('val-vscale');
const infoTheta = document.getElementById('info-theta');
const infoGForce = document.getElementById('info-gforce');
// Physics Constants
const g = 9.81;
const dt_base = 0.016; // 60 FPS target
let simState = {
running: false,
theta: Math.PI / 4, // 45 degrees start
omega: 0,
L: 2.0, // meters
timeScale: 1.0,
vectorScale: 0.8, // Default scale
dragging: false
};
// Drawing params
let originX, originY, scalePixels; // Pixels per meter
// --- PHYSICS UPDATE ---
function updatePhysics() {
if (!simState.running || simState.dragging) return;
// Effective dt with slow-motion
const dt = dt_base * simState.timeScale;
// Semi-Implicit Euler
const damping = 0.999;
const alpha = -(g / simState.L) * Math.sin(simState.theta);
simState.omega += alpha * dt;
simState.omega *= damping;
simState.theta += simState.omega * dt;
}
// --- VECTOR DRAWING HELPER ---
function drawVector(ctx, x, y, vx, vy, color, label, isDashed = false) {
const headLen = 8;
const len = Math.sqrt(vx*vx + vy*vy);
if (len < 1) return;
ctx.save();
ctx.beginPath();
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.lineWidth = 2;
if (isDashed) ctx.setLineDash([5, 3]);
// Line
ctx.moveTo(x, y);
ctx.lineTo(x + vx, y + vy);
ctx.stroke();
// Arrow head
ctx.setLineDash([]);
const angle = Math.atan2(vy, vx);
ctx.beginPath();
ctx.moveTo(x + vx, y + vy);
ctx.lineTo(x + vx - headLen * Math.cos(angle - Math.PI / 6), y + vy - headLen * Math.sin(angle - Math.PI / 6));
ctx.lineTo(x + vx - headLen * Math.cos(angle + Math.PI / 6), y + vy - headLen * Math.sin(angle + Math.PI / 6));
ctx.fill();
// Label
if (label) {
ctx.font = "bold 11px Arial";
ctx.fillStyle = color;
ctx.fillText(label, x + vx + 8, y + vy);
}
ctx.restore();
}
// --- MAIN DRAW LOOP ---
function draw() {
// FIX: Use canvas dimensions directly, which match the container due to ResizeObserver
const width = canvas.width;
const height = canvas.height;
ctx.clearRect(0, 0, width, height);
// Coordinate System
originX = width / 2;
originY = height * 0.2;
const maxL_pixels = Math.min(width/2 - 40, height * 0.75);
scalePixels = maxL_pixels / 4.0;
// Bob Position
const bobX = originX + simState.L * scalePixels * Math.sin(simState.theta);
const bobY = originY + simState.L * scalePixels * Math.cos(simState.theta);
// 1. Ceiling & Pivot
ctx.strokeStyle = "#94a3b8";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(originX - 50, originY);
ctx.lineTo(originX + 50, originY);
ctx.stroke();
// Vertical Dash Line
ctx.setLineDash([5, 5]);
ctx.strokeStyle = "#e2e8f0";
ctx.beginPath();
ctx.moveTo(originX, originY);
ctx.lineTo(originX, height);
ctx.stroke();
ctx.setLineDash([]);
// 2. String
ctx.strokeStyle = "#334155";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(originX, originY);
ctx.lineTo(bobX, bobY);
ctx.stroke();
// Pivot Point
ctx.fillStyle = "#64748b";
ctx.beginPath();
ctx.arc(originX, originY, 4, 0, Math.PI * 2);
ctx.fill();
// 3. Bob
ctx.fillStyle = "#f59e0b";
ctx.beginPath();
ctx.arc(bobX, bobY, 15, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
// --- VECTOR CALCULATIONS ---
const vScale = 35 * simState.vectorScale;
const sinT = Math.sin(simState.theta);
const cosT = Math.cos(simState.theta);
// A. Gravity (mg)
const Fg_mag = g;
const Fg_x = 0;
const Fg_y = Fg_mag * vScale;
// B. Gravity Components
const Fg_rad_mag = g * cosT;
const Fg_rad_x = Fg_rad_mag * sinT * vScale;
const Fg_rad_y = Fg_rad_mag * cosT * vScale;
const Fg_tan_mag = -g * sinT;
const Fg_tan_x = Fg_tan_mag * cosT * vScale;
const Fg_tan_y = Fg_tan_mag * (-sinT) * vScale;
// C. Tension
const v = simState.omega * simState.L;
const a_centripetal = (v * v) / simState.L;
const T_mag = g * cosT + a_centripetal;
const T_x = -sinT * T_mag * vScale;
const T_y = -cosT * T_mag * vScale;
// D. Net Force
const Fnet_x = T_x + Fg_x;
const Fnet_y = T_y + Fg_y;
// E. Velocity
const vel_x = v * cosT * (vScale * 0.5);
const vel_y = v * (-sinT) * (vScale * 0.5);
// F. Acceleration
const acc_tan_mag = -g * sinT;
const acc_rad_mag = (v * v) / simState.L;
const acc_tan_x = acc_tan_mag * cosT;
const acc_tan_y = acc_tan_mag * (-sinT);
const acc_rad_x = -acc_rad_mag * sinT;
const acc_rad_y = -acc_rad_mag * cosT;
const acc_total_x = (acc_tan_x + acc_rad_x) * vScale;
const acc_total_y = (acc_tan_y + acc_rad_y) * vScale;
// --- SEQUENTIAL DRAWING ---
// 1. Gravity Components (dashed underlay)
if (chkComponents.checked) {
drawVector(ctx, bobX, bobY, Fg_rad_x, Fg_rad_y, "#94a3b8", "mg cosθ", true);
ctx.save();
ctx.setLineDash([5, 3]);
ctx.strokeStyle = "#94a3b8";
ctx.beginPath();
ctx.moveTo(bobX + Fg_rad_x, bobY + Fg_rad_y);
ctx.lineTo(bobX + Fg_rad_x + Fg_tan_x, bobY + Fg_rad_y + Fg_tan_y);
ctx.stroke();
drawVector(ctx, bobX + Fg_rad_x, bobY + Fg_rad_y, Fg_tan_x, Fg_tan_y, "#94a3b8", "", true);
ctx.restore();
}
// 2. Gravity
if (chkGravity.checked) {
drawVector(ctx, bobX, bobY, Fg_x, Fg_y, "#ef4444", "Fg");
}
// 3. Tension
if (chkTension.checked) {
drawVector(ctx, bobX, bobY, T_x, T_y, "#854d0e", "T");
}
// 4. Net Force
if (chkNet.checked) {
drawVector(ctx, bobX, bobY, Fnet_x, Fnet_y, "#9333ea", "F_net");
}
// 5. Velocity
if (chkVelocity.checked) {
drawVector(ctx, bobX, bobY, vel_x, vel_y, "#16a34a", "v");
}
// 6. Acceleration
if (chkAccel.checked) {
drawVector(ctx, bobX, bobY, acc_total_x, acc_total_y, "#f97316", "a");
}
// --- TEXT UI UPDATE ---
let deg = Math.round(simState.theta * 180 / Math.PI);
infoTheta.innerText = deg;
const gForce = T_mag / g;
infoGForce.innerText = gForce.toFixed(2);
}
// --- ANIMATION LOOP ---
function loop() {
updatePhysics();
draw();
requestAnimationFrame(loop);
}
// --- INTERACTION ---
// Resize Canvas properly - DEBOUNCED / RAF
let resizeRequestId = null;
const resizeObserver = new ResizeObserver(() => {
if (!resizeRequestId) {
resizeRequestId = window.requestAnimationFrame(() => {
const rect = canvas.parentElement.getBoundingClientRect();
// Set canvas props to match the parent div
canvas.width = rect.width;
canvas.height = rect.height;
if (!simState.running) draw();
resizeRequestId = null;
});
}
});
// OBSERVE THE WRAPPER OR CANVAS PARENT, NOT WINDOW
resizeObserver.observe(canvas.parentElement);
// Dragging Bob
function getMousePos(evt) {
const rect = canvas.getBoundingClientRect();
const clientX = evt.clientX || evt.touches[0].clientX;
const clientY = evt.clientY || evt.touches[0].clientY;
return {
x: clientX - rect.left,
y: clientY - rect.top
};
}
function handleStart(evt) {
const pos = getMousePos(evt);
const bobX = originX + simState.L * scalePixels * Math.sin(simState.theta);
const bobY = originY + simState.L * scalePixels * Math.cos(simState.theta);
const dist = Math.hypot(pos.x - bobX, pos.y - bobY);
if (dist < 40) {
simState.dragging = true;
simState.running = false;
btnPlay.textContent = "START";
}
}
function handleMove(evt) {
if (!simState.dragging) return;
evt.preventDefault();
const pos = getMousePos(evt);
const dx = pos.x - originX;
const dy = pos.y - originY;
let angle = Math.atan2(dx, dy);
if (angle > Math.PI/1.8) angle = Math.PI/1.8;
if (angle < -Math.PI/1.8) angle = -Math.PI/1.8;
simState.theta = angle;
simState.omega = 0;
draw();
}
function handleEnd() {
simState.dragging = false;
}
canvas.addEventListener('mousedown', handleStart);
canvas.addEventListener('mousemove', handleMove);
window.addEventListener('mouseup', handleEnd);
canvas.addEventListener('touchstart', handleStart, {passive: false});
canvas.addEventListener('touchmove', handleMove, {passive: false});
window.addEventListener('touchend', handleEnd);
// --- CONTROLS LISTENERS ---
btnPlay.addEventListener('click', () => {
simState.running = !simState.running;
btnPlay.textContent = simState.running ? "PAUSE" : "START";
});
btnReset.addEventListener('click', () => {
simState.running = false;
btnPlay.textContent = "START";
simState.theta = Math.PI / 4;
simState.omega = 0;
draw();
});
rngSpeed.addEventListener('input', (e) => {
simState.timeScale = parseFloat(e.target.value);
valSpeed.textContent = simState.timeScale.toFixed(1) + "x";
});
rngLength.addEventListener('input', (e) => {
simState.L = parseFloat(e.target.value);
valLength.textContent = simState.L.toFixed(1) + "m";
if (!simState.running) draw();
});
rngVScale.addEventListener('input', (e) => {
simState.vectorScale = parseFloat(e.target.value);
valVScale.textContent = Math.round(simState.vectorScale * 100) + "%";
if (!simState.running) draw();
});
[chkGravity, chkComponents, chkTension, chkNet, chkVelocity, chkAccel].forEach(chk => {
chk.addEventListener('change', () => {
if (!simState.running) draw();
});
});
// Start loop
loop();
})();
</script>
</div>
<!-- PENDULUM FORCES LAB COMPONENT END -->
```
#### Analytical Solution
The analytical solution for small angles is:
$$
\theta(t) = \theta_0 \cos(\omega t + \phi)
$$
where:
- $\theta_0$ is the initial angular displacement.
- $\phi$ is the phase constant, determined by initial conditions.
#### Numerical Solution
For larger angles, the small-angle approximation does not hold, and we need to solve the original nonlinear equation numerically. We can use the finite difference method to approximate the solution.
```{python}
import numpy as np
import matplotlib.pyplot as plt
# Parameters
g = 9.81 # Acceleration due to gravity (m/s^2)
L = 1.0 # Length of the pendulum (m)
theta_0 = np.pi / 4 # Initial angle (radians)
# Time settings
T = 10 # Total time (s)
N = 10_000 # Number of time steps
dt = T / N
# Arrays to store values
t = np.linspace(0, T, N)
theta_full = np.zeros(N)
omega_full = np.zeros(N) # Angular velocity for full sin(theta)
theta_approx = np.zeros(N)
omega_approx = np.zeros(N) # Angular velocity for sin(theta) ~ theta
# Initial conditions
theta_full[0] = theta_0
omega_full[0] = 0
theta_approx[0] = theta_0
omega_approx[0] = 0
# Numerical integration (Euler method)
for i in range(1, N):
# Full sin(theta)
alpha_full = -(g / L) * np.sin(theta_full[i - 1])
omega_full[i] = omega_full[i - 1] + alpha_full * dt
theta_full[i] = theta_full[i - 1] + omega_full[i] * dt
# Approximation sin(theta) ~ theta
alpha_approx = -(g / L) * theta_approx[i - 1]
omega_approx[i] = omega_approx[i - 1] + alpha_approx * dt
theta_approx[i] = theta_approx[i - 1] + omega_approx[i] * dt
# Plotting
plt.figure(figsize=(12, 6))
plt.plot(t, theta_full, label="Full sin(theta)", color="blue")
plt.plot(t, theta_approx, label="sin(theta) ~ theta", color="red", linestyle="--")
plt.title("Pendulum Motion: Full sin(theta) vs Approximation")
plt.xlabel("Time (s)")
plt.ylabel("Angle (radians)")
plt.grid()
plt.legend()
plt.show()
```
```{=html}
<!-- PENDULUM SIMULATION COMPONENT START -->
<div id="pendulum-sim-wrapper">
<style>
/* Scoped CSS - affects only elements inside #pendulum-sim-wrapper */
#pendulum-sim-wrapper {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f3f4f6;
color: #1f2937;
padding: 1rem;
box-sizing: border-box;
width: 100%;
height: 100%;
min-height: 600px; /* Default height if parent doesn't constrain it */
display: flex;
flex-direction: column;
overflow: hidden; /* Only clip content inside the app, not the page */
border-radius: 8px;
border: 1px solid #e5e7eb;
}
#pendulum-sim-wrapper * {
box-sizing: border-box;
}
#pendulum-sim-wrapper .layout-container {
display: flex;
flex-direction: column;
gap: 1rem;
height: 100%;
}
/* Responsive layout for larger screens (simulate Tailwind md:flex-row) */
@media (min-width: 768px) {
#pendulum-sim-wrapper .layout-container {
flex-direction: row;
}
}
#pendulum-sim-wrapper .panel {
background-color: #ffffff;
padding: 1rem;
border-radius: 8px;
border: 1px solid #e5e7eb;
flex: 0 0 auto;
display: flex;
flex-direction: column;
gap: 1.5rem;
width: 100%;
}
@media (min-width: 768px) {
#pendulum-sim-wrapper .panel {
width: 33.333%;
}
}
#pendulum-sim-wrapper h1 {
font-size: 1.25rem;
font-weight: 700;
border-bottom: 1px solid #d1d5db;
padding-bottom: 0.5rem;
margin: 0;
line-height: 1.2;
}
#pendulum-sim-wrapper label {
display: block;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 0.25rem;
}
#pendulum-sim-wrapper .control-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
#pendulum-sim-wrapper input[type=range] {
width: 100%;
cursor: pointer;
accent-color: #3b82f6;
}
#pendulum-sim-wrapper .value-display {
font-family: 'Courier New', monospace;
font-weight: bold;
width: 3rem;
text-align: right;
flex-shrink: 0;
}
#pendulum-sim-wrapper .info-text {
font-size: 0.75rem;
color: #6b7280;
margin-top: 0.25rem;
margin-bottom: 0;
}
#pendulum-sim-wrapper .button-group {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
}
#pendulum-sim-wrapper button {
flex: 1;
padding: 0.5rem 1rem;
border-radius: 0.25rem;
font-weight: 700;
cursor: pointer;
border: none;
transition: background-color 0.2s;
font-size: 1rem;
}
#pendulum-sim-wrapper #btnStart {
background-color: #2563eb;
color: white;
}
#pendulum-sim-wrapper #btnStart:hover {
background-color: #1d4ed8;
}
#pendulum-sim-wrapper #btnPause {
background-color: #4b5563;
color: white;
}
#pendulum-sim-wrapper #btnPause:hover {
background-color: #374151;
}
#pendulum-sim-wrapper .stats-container {
margin-top: auto;
background-color: #f9fafb;
padding: 1rem;
border-radius: 0.25rem;
border: 1px solid #e5e7eb;
font-size: 0.875rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
#pendulum-sim-wrapper .stat-box {
padding-left: 0.5rem;
border-left-width: 4px;
border-left-style: solid;
}
#pendulum-sim-wrapper .stat-box.blue { border-left-color: #3b82f6; }
#pendulum-sim-wrapper .stat-box.red { border-left-color: #ef4444; }
#pendulum-sim-wrapper .stat-title {
font-weight: 700;
margin: 0 0 0.25rem 0;
}
#pendulum-sim-wrapper .text-blue { color: #2563eb; }
#pendulum-sim-wrapper .text-red { color: #dc2626; }
#pendulum-sim-wrapper .text-orange { color: #ea580c; }
#pendulum-sim-wrapper .text-gray { color: #1f2937; }
#pendulum-sim-wrapper p { margin: 0; line-height: 1.5; }
#pendulum-sim-wrapper .canvas-container {
flex: 1;
position: relative;
background-color: #ffffff;
border-radius: 8px;
border: 1px solid #d1d5db;
overflow: hidden;
min-height: 300px; /* Ensure visibility on mobile */
}
#pendulum-sim-wrapper canvas {
display: block;
width: 100%;
height: 100%;
}
#pendulum-sim-wrapper .legend {
position: absolute;
top: 0.5rem;
left: 0.5rem;
font-size: 0.75rem;
color: #6b7280;
pointer-events: none;
background: rgba(255,255,255,0.8);
padding: 2px 4px;
border-radius: 4px;
}
</style>
<div class="layout-container">
<!-- Controls Panel -->
<div class="panel">
<h1>Parameters</h1>
<!-- Length Control -->
<div>
<label>String Length (L) [m]</label>
<div class="control-group">
<input type="range" id="sim-lengthRange" min="0.5" max="3.0" step="0.1" value="2.0">
<span id="sim-lengthVal" class="value-display">2.0</span>
</div>
</div>
<!-- Angle Control -->
<div>
<label>Start Angle [deg]</label>
<div class="control-group">
<input type="range" id="sim-angleRange" min="5" max="179" step="1" value="90">
<span id="sim-angleVal" class="value-display">90°</span>
</div>
<p class="info-text">For small angles (<10°), pendulums overlap.</p>
</div>
<div class="button-group">
<button id="sim-btnStart">START / RESET</button>
<button id="sim-btnPause">PAUSE</button>
</div>
<!-- Data Display -->
<div class="stats-container">
<div class="stat-box blue">
<p class="stat-title text-blue">Real Pendulum (sin θ)</p>
<p>Tangential Force: <span id="sim-realForce" class="value-display text-gray">0.00</span> g</p>
<p>Measured Period (T): <span id="sim-realPeriod" class="value-display text-orange">---</span> s</p>
<p class="info-text">Theoretical > 2π√(L/g)</p>
</div>
<div class="stat-box red">
<p class="stat-title text-red">Linear Pendulum (θ)</p>
<p>Tangential Force: <span id="sim-linForce" class="value-display text-gray">0.00</span> g</p>
<p>Measured Period (T): <span id="sim-linPeriod" class="value-display text-orange">---</span> s</p>
<p class="info-text">Theoretical = <span id="sim-theoPeriod">---</span> s</p>
</div>
<div style="padding-top: 0.5rem; border-top: 1px solid #d1d5db; font-size: 0.75rem;">
<p>Initial Force Diff: <span id="sim-forceDiff" style="font-weight: bold; color: #dc2626;">0%</span></p>
<p class="info-text" style="font-style: italic;">If > 0%, linear model "cheats".</p>
</div>
</div>
</div>
<!-- Visualization -->
<div class="canvas-container">
<canvas id="sim-canvas"></canvas>
<div class="legend">
Blue: Real<br>
Red: Linear
</div>
</div>
</div>
<script>
(function() {
// Scoped variable definitions to prevent leaks
// Added 'sim-' prefix to IDs to further reduce collision risk if pasted blindly
const wrapper = document.getElementById('pendulum-sim-wrapper');
const canvas = document.getElementById('sim-canvas');
const ctx = canvas.getContext('2d');
const lengthInput = document.getElementById('sim-lengthRange');
const angleInput = document.getElementById('sim-angleRange');
const btnStart = document.getElementById('sim-btnStart');
const btnPause = document.getElementById('sim-btnPause');
const dispL = document.getElementById('sim-lengthVal');
const dispA = document.getElementById('sim-angleVal');
const dispRealF = document.getElementById('sim-realForce');
const dispLinF = document.getElementById('sim-linForce');
const dispRealT = document.getElementById('sim-realPeriod');
const dispLinT = document.getElementById('sim-linPeriod');
const dispTheoT = document.getElementById('sim-theoPeriod');
const dispDiff = document.getElementById('sim-forceDiff');
const g = 9.81;
const dt = 0.01;
let simRunning = false;
let animationId;
let L = 2.0;
let startAngleDeg = 90;
let startAngleRad = Math.PI / 2;
const pReal = {
theta: 0, omega: 0,
color: '#2563eb', label: 'Real',
period: 0, lastZeroCrossing: 0, halfPeriods: 0, startTime: 0, periodHistory: []
};
const pLin = {
theta: 0, omega: 0,
color: '#dc2626', label: 'Linear',
period: 0, lastZeroCrossing: 0, halfPeriods: 0, startTime: 0, periodHistory: []
};
let scale = 150;
let originX = 0;
let originY = 50;
function updateView() {
// Check dimensions of the container, NOT the window
if (!canvas.parentElement) return;
const rect = canvas.parentElement.getBoundingClientRect();
// Handle high DPI displays
const dpr = window.devicePixelRatio || 1;
// Avoid setting if dimensions haven't changed to minimize paint/layout thrashing
// but for simplicity and robustness we set it, trusting requestAnimationFrame from ResizeObserver
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
// Normalize coordinate system so code works with logical pixels
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
const width = rect.width;
const height = rect.height;
originX = width / 2;
originY = height * 0.35;
const maxH_down = height - originY - 40;
const availableSize = Math.min(width * 0.45, height * 0.55);
scale = availableSize / L;
if (!simRunning) draw();
}
function reset() {
simRunning = false;
cancelAnimationFrame(animationId);
L = parseFloat(lengthInput.value);
startAngleDeg = parseFloat(angleInput.value);
startAngleRad = startAngleDeg * (Math.PI / 180);
updateView();
pReal.theta = startAngleRad; pReal.omega = 0;
pReal.halfPeriods = 0; pReal.periodHistory = []; pReal.lastZeroCrossing = 0;
pLin.theta = startAngleRad; pLin.omega = 0;
pLin.halfPeriods = 0; pLin.periodHistory = []; pLin.lastZeroCrossing = 0;
const T_theo = 2 * Math.PI * Math.sqrt(L / g);
dispTheoT.textContent = T_theo.toFixed(3);
dispRealT.textContent = "---";
dispLinT.textContent = "---";
const fReal = Math.sin(startAngleRad);
const fLin = startAngleRad;
const diffPerc = ((fLin - fReal) / fReal) * 100;
dispDiff.textContent = isFinite(diffPerc) ? `+${diffPerc.toFixed(1)}%` : "Infinity";
draw();
btnStart.textContent = "START";
}
function updatePhysics() {
const alphaReal = -(g / L) * Math.sin(pReal.theta);
pReal.omega += alphaReal * dt;
const prevThetaReal = pReal.theta;
pReal.theta += pReal.omega * dt;
if ((prevThetaReal > 0 && pReal.theta <= 0) || (prevThetaReal < 0 && pReal.theta >= 0)) {
const now = performance.now();
if (pReal.halfPeriods > 0) {
const currentT = (now - pReal.lastZeroCrossing) * 2 / 1000;
pReal.periodHistory.push(currentT);
if (pReal.periodHistory.length > 5) pReal.periodHistory.shift();
const avgT = pReal.periodHistory.reduce((a,b)=>a+b)/pReal.periodHistory.length;
dispRealT.textContent = avgT.toFixed(3);
}
pReal.lastZeroCrossing = now;
pReal.halfPeriods++;
}
const alphaLin = -(g / L) * pLin.theta;
pLin.omega += alphaLin * dt;
const prevThetaLin = pLin.theta;
pLin.theta += pLin.omega * dt;
if ((prevThetaLin > 0 && pLin.theta <= 0) || (prevThetaLin < 0 && pLin.theta >= 0)) {
const now = performance.now();
if (pLin.halfPeriods > 0) {
const currentT = (now - pLin.lastZeroCrossing) * 2 / 1000;
pLin.periodHistory.push(currentT);
if (pLin.periodHistory.length > 5) pLin.periodHistory.shift();
const avgT = pLin.periodHistory.reduce((a,b)=>a+b)/pLin.periodHistory.length;
dispLinT.textContent = avgT.toFixed(3);
}
pLin.lastZeroCrossing = now;
pLin.halfPeriods++;
}
dispRealF.textContent = Math.abs(Math.sin(pReal.theta)).toFixed(3);
dispLinF.textContent = Math.abs(pLin.theta).toFixed(3);
}
function drawPendulum(ctx, theta, lengthPx, color, isGhost, forceVal) {
const x = originX + lengthPx * Math.sin(theta);
const y = originY + lengthPx * Math.cos(theta);
ctx.lineWidth = 2;
ctx.strokeStyle = color;
ctx.setLineDash(isGhost ? [5, 5] : []);
ctx.globalAlpha = isGhost ? 0.7 : 1.0;
ctx.beginPath();
ctx.moveTo(originX, originY);
ctx.lineTo(x, y);
ctx.stroke();
ctx.beginPath();
ctx.arc(x, y, 12, 0, 2 * Math.PI);
ctx.fillStyle = color;
ctx.fill();
const forceScale = 60;
const forceMag = Math.abs(forceVal);
const direction = theta > 0 ? -1 : 1;
const vX = Math.cos(theta) * direction * forceMag * forceScale;
const vY = -Math.sin(theta) * direction * forceMag * forceScale;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + vX, y + vY);
ctx.lineWidth = 4;
ctx.strokeStyle = isGhost ? '#fca5a5' : '#93c5fd';
ctx.stroke();
const headLen = 8;
const angleVec = Math.atan2(vY, vX);
ctx.beginPath();
ctx.moveTo(x + vX, y + vY);
ctx.lineTo(x + vX - headLen * Math.cos(angleVec - Math.PI / 6), y + vY - headLen * Math.sin(angleVec - Math.PI / 6));
ctx.lineTo(x + vX - headLen * Math.cos(angleVec + Math.PI / 6), y + vY - headLen * Math.sin(angleVec + Math.PI / 6));
ctx.fillStyle = isGhost ? '#fca5a5' : '#93c5fd';
ctx.fill();
ctx.globalAlpha = 1.0;
ctx.setLineDash([]);
}
function draw() {
// Since we setTransform with dpr, logical coords work fine,
// but clearRect needs actual canvas dims or logical dims.
// With setTransform, logical dims work.
if (!canvas.parentElement) return;
const rect = canvas.parentElement.getBoundingClientRect();
ctx.clearRect(0, 0, rect.width, rect.height);
ctx.fillStyle = '#9ca3af';
ctx.beginPath();
ctx.arc(originX, originY, 5, 0, 2*Math.PI);
ctx.fill();
ctx.strokeStyle = '#d1d5db';
ctx.setLineDash([2, 4]);
ctx.beginPath();
ctx.moveTo(originX, 0);
ctx.lineTo(originX, rect.height);
ctx.stroke();
const lenPx = L * scale;
drawPendulum(ctx, pLin.theta, lenPx, pLin.color, true, pLin.theta);
drawPendulum(ctx, pReal.theta, lenPx, pReal.color, false, Math.sin(pReal.theta));
}
function loop() {
if (!simRunning) return;
updatePhysics();
updatePhysics();
draw();
animationId = requestAnimationFrame(loop);
}
lengthInput.addEventListener('input', (e) => {
dispL.textContent = parseFloat(e.target.value).toFixed(1);
if (!simRunning) reset();
});
angleInput.addEventListener('input', (e) => {
dispA.textContent = e.target.value + '°';
if (!simRunning) reset();
});
btnStart.addEventListener('click', () => {
if (simRunning) {
reset();
} else {
if (pReal.periodHistory.length === 0) reset();
simRunning = true;
pReal.lastZeroCrossing = performance.now();
pLin.lastZeroCrossing = performance.now();
loop();
btnStart.textContent = "RESET";
}
});
btnPause.addEventListener('click', () => {
simRunning = !simRunning;
if (simRunning) {
pReal.lastZeroCrossing = performance.now();
pLin.lastZeroCrossing = performance.now();
loop();
btnPause.textContent = "PAUSE";
} else {
btnPause.textContent = "RESUME";
}
});
// FIX: Use a debounced observer on the specific container to prevent loop errors
let resizeRequestId;
const resizeObserver = new ResizeObserver(() => {
if (!resizeRequestId) {
resizeRequestId = window.requestAnimationFrame(() => {
updateView();
resizeRequestId = null;
});
}
});
// Observe only the canvas container
resizeObserver.observe(canvas.parentElement);
// Init
setTimeout(reset, 100); // Small delay to ensure layout is ready
})();
</script>
</div>
<!-- PENDULUM SIMULATION COMPONENT END -->
```
#### Energy Analysis
The total mechanical energy of the pendulum is conserved (assuming no damping). The total energy is the sum of kinetic and potential energy:
$$
E = K + U
$$
- Kinetic energy: $K = \frac{1}{2} m v^2$
- Potential energy: $U = m g h$
For a pendulum:
$$
v = L \frac{d\theta}{dt}, \quad h = L (1 - \cos\theta)
$$
Thus, the total energy becomes:
$$
E = \frac{1}{2} m L^2 \left(\frac{d\theta}{dt}\right)^2 + m g L (1 - \cos\theta)
$$
The energy remains constant over time, which can be verified numerically.
```{python}
# Calculate energies
m=1
kinetic_energy = 0.5 * m * (L * omega_full)**2
potential_energy = m * g * L * (1 - np.cos(theta_full))
total_energy = kinetic_energy + potential_energy
# Plot energies
plt.figure(figsize=(10, 6))
plt.plot(t, kinetic_energy, label="Kinetic Energy")
plt.plot(t, potential_energy, label="Potential Energy")
plt.plot(t, total_energy, label="Total Energy", linestyle="dashed")
plt.title("Energy of the Pendulum")
plt.xlabel("Time (s)")
plt.ylabel("Energy (J)")
plt.grid()
plt.legend()
plt.show()
```
### Circular Motion
#### Introduction
Circular motion refers to the movement of an object along a circular path. This motion can be uniform (constant speed) or non-uniform (variable speed). For simplicity, we will consider uniform circular motion, where the speed of the object is constant. The position, velocity, and acceleration vectors in circular motion exhibit interesting properties:
- The velocity vector is always tangent to the circle.
- The acceleration vector (centripetal acceleration) always points toward the center of the circle.
#### Equations of Motion
Assume an object moves along a circular path of radius $R$ with constant angular velocity $\omega$.
##### Position Vector
The position of the object at time $t$ can be described in terms of the angle $\theta(t)$ it makes with the reference axis:
$$
\mathbf{r}(t) = R \cos(\omega t) \hat{i} + R \sin(\omega t) \hat{j}
$$
#### Velocity Vector
The velocity is the derivative of the position vector with respect to time:
$$
\mathbf{v}(t) = \frac{d\mathbf{r}(t)}{dt} = -R \omega \sin(\omega t) \hat{i} + R \omega \cos(\omega t) \hat{j}
$$
The magnitude of the velocity is constant and equals:
$$
|\mathbf{v}(t)| = R \omega
$$
**Velocity and Position Perpendicularity:**
The dot product of the position vector $\mathbf{r}(t)$ and velocity vector $\mathbf{v}(t)$ is:
$$
\mathbf{r}(t) \cdot \mathbf{v}(t) = \left[R \cos(\omega t)\right] \left[-R \omega \sin(\omega t)\right] + \left[R \sin(\omega t)\right] \left[R \omega \cos(\omega t)\right]
$$
Simplifying:
$$
\mathbf{r}(t) \cdot \mathbf{v}(t) = -R^2 \omega \cos(\omega t) \sin(\omega t) + R^2 \omega \sin(\omega t) \cos(\omega t) = 0
$$
This confirms that $\mathbf{r}(t)$ and $\mathbf{v}(t)$ are perpendicular.
#### Acceleration Vector
The acceleration is the derivative of the velocity vector with respect to time:
$$
\mathbf{a}(t) = \frac{d\mathbf{v}(t)}{dt} = -R \omega^2 \cos(\omega t) \hat{i} - R \omega^2 \sin(\omega t) \hat{j} = -R \omega^2 \mathbf{r}(t)
$$
The acceleration vector always points toward the center of the circle (centripetal acceleration), and its magnitude is:
$$
|\mathbf{a}(t)| = R \omega^2 = \frac{v^2}{R}
$$
Last relation is very important because it shows that the acceleration is proportional to the square of the velocity and inversely proportional to the radius of the circle.
This result will we very useful in the next section when we will discuss the gravity.
#### Numerical Example
We will visualize the position, velocity, and acceleration at two distinct points along the circular path.
```{python}
import numpy as np
import matplotlib.pyplot as plt
# Parameters
R = 1 # Radius of the circle
omega = np.pi/4 # Angular velocity (rad/s)
T = 1 # Time period (s)
t_points = [0, T] # Time points to analyze
# Calculate vectors at specific points
positions = [(R * np.cos(omega * t), R * np.sin(omega * t)) for t in t_points]
velocities = [(-R * omega * np.sin(omega * t), R * omega * np.cos(omega * t)) for t in t_points]
accelerations = [(-R * omega**2 * np.cos(omega * t), -R * omega**2 * np.sin(omega * t)) for t in t_points]
# Plot the circle and vectors
fig, ax = plt.subplots(figsize=(8, 8))
theta = np.linspace(0, 2 * np.pi, 100)
x_circle = R * np.cos(theta)
y_circle = R * np.sin(theta)
ax.plot(x_circle, y_circle, label="Circular Path")
for i, t in enumerate(t_points):
x, y = positions[i]
vx, vy = velocities[i]
axx, axy = accelerations[i]
ax.quiver(x, y, vx, vy, color="red", angles="xy", scale_units="xy", scale=1, label=f"Velocity at t={t:.2f}s" if i == 0 else None)
ax.quiver(x, y, axx, axy, color="blue", angles="xy", scale_units="xy", scale=1, label=f"Acceleration at t={t:.2f}s" if i == 0 else None)
ax.set_aspect('equal', adjustable='box')
ax.set_title("Circular Motion: Velocity and Acceleration")
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_xlim(-2, 2)
ax.set_ylim(-2, 2)
ax.legend()
ax.grid()
plt.show()
```
## Gravity
### Introduction
The Universal Law of Gravitation, formulated by Sir Isaac Newton, describes the gravitational force between two masses. It states that every particle of matter in the universe attracts every other particle with a force that is directly proportional to the product of their masses and inversely proportional to the square of the distance between their centers.
### Mathematical Formulation
The gravitational force $\mathbf{F}$ between two masses $m_1$ and $m_2$ separated by a distance $r$ is given by:
$$
\mathbf{F} = -G \frac{m_1 m_2}{r^2} \hat{r}
$$
Where:
- $G$ is the gravitational constant ($6.674 \times 10^{-11} \ \mathrm{Nm^2/kg^2}$),
- $\hat{r}$ is the unit vector pointing from one mass to the other,
- $m_1$ and $m_2$ are the masses of the two bodies,
- $r$ is the distance between the centers of the masses.
### Gravitational Field
The gravitational field $\mathbf{g}$ at a distance $r$ from a mass $M$ is defined as the force per unit mass exerted by $M$:
$$
\mathbf{g} = -G \frac{M}{r^2} \hat{r}
$$
This field points toward the mass $M$ and has a magnitude:
$$
|\mathbf{g}| = G \frac{M}{r^2}
$$
#### Visualization: Gravitational Field
We visualize the gravitational field of a central mass $M$ in 2D slice.
```{python}
import numpy as np
import matplotlib.pyplot as plt
# Parameters
M = 2e22 # Mass of central body (kg)
G = 6.674e-11 # Gravitational constant
# Grid for visualization
x = np.linspace(-100, 100, 25)
y = np.linspace(-100, 100, 25)
X, Y = np.meshgrid(x, y)
R = np.sqrt(X**2 + Y**2)
# Avoid division by zero
R[R == 0] = np.nan
R[R <35] = np.nan
# Gravitational field
Fx = -G * M * X / R**3
Fy = -G * M * Y / R**3
# Plot field
plt.figure(figsize=(8, 8))
plt.quiver(X, Y, Fx, Fy, scale=1e10, color='blue')
plt.xlabel('x (m)')
plt.ylabel('y (m)')
plt.title('Gravitational Field')
plt.grid()
plt.gca().set_aspect('equal', adjustable='box')
plt.show()
```
### Orbital Motion
Gravitational force is responsible for the orbital motion of planets, moons, and satellites. The centripetal force required for circular motion is provided by gravity:
$$
F = \frac{mv^2}{r} = G \frac{mM}{r^2}
$$
From this, the orbital velocity $v$ of a body of mass $m$ around a central mass $M$ is:
$$
v = \sqrt{G \frac{M}{r}}
$$
The orbital period $T$ of the body can also be derived:
$$
T = 2\pi \sqrt{\frac{r^3}{GM}}
$$
```{=html}
<!-- ORBITAL MECHANICS SIMULATOR START -->
<div id="orbit-sim-wrapper">
<style>
/* SCOPED CSS FOR ORBIT SIMULATOR - LIGHT THEME */
#orbit-sim-wrapper {
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
background-color: #ffffff; /* White background */
color: #334155; /* Slate-700 text */
padding: 0;
margin: 0;
box-sizing: border-box;
width: 100%;
height: auto;
min-height: 600px;
display: flex;
flex-direction: column;
border: 1px solid #cbd5e1; /* Lighter border */
border-radius: 8px;
overflow: hidden;
position: relative;
}
#orbit-sim-wrapper * {
box-sizing: border-box;
}
/* LAYOUT */
#orbit-sim-wrapper .app-layout {
display: flex;
flex-direction: column;
min-height: 600px;
flex: 1;
}
@media (min-width: 900px) {
#orbit-sim-wrapper .app-layout {
flex-direction: row;
}
}
/* CONTROLS AREA (LEFT) */
#orbit-sim-wrapper .controls-area {
width: 100%;
background-color: #f8fafc; /* Slate-50 */
border-right: 1px solid #cbd5e1;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
overflow-y: auto;
order: 1;
z-index: 2;
}
@media (min-width: 900px) {
#orbit-sim-wrapper .controls-area {
width: 320px;
flex-shrink: 0;
height: auto;
max-height: none;
}
}
/* CANVAS AREA (RIGHT) */
#orbit-sim-wrapper .canvas-area {
flex: 1;
position: relative;
background-color: #ffffff; /* White canvas */
overflow: hidden;
min-height: 400px;
cursor: crosshair;
order: 2;
}
#orbit-sim-wrapper canvas {
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
/* UI ELEMENTS */
#orbit-sim-wrapper h2 {
margin: 0 0 0.25rem 0;
font-size: 1.5rem;
color: #0f172a; /* Slate-900 */
font-weight: 700;
}
#orbit-sim-wrapper .subtitle {
font-size: 0.85rem;
color: #64748b; /* Slate-500 */
margin-bottom: 1rem;
}
#orbit-sim-wrapper .section-header {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #475569; /* Slate-600 */
font-weight: 700;
margin-bottom: 0.5rem;
border-bottom: 1px solid #e2e8f0;
padding-bottom: 0.25rem;
}
/* BUTTONS GRID */
#orbit-sim-wrapper .presets-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
margin-bottom: 1rem;
}
#orbit-sim-wrapper button {
padding: 0.6rem;
border: 1px solid #cbd5e1;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
font-size: 0.9rem;
background-color: #ffffff;
color: #334155;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
#orbit-sim-wrapper button:hover {
background-color: #f1f5f9;
border-color: #94a3b8;
}
#orbit-sim-wrapper button.active {
background-color: #f0f9ff; /* Very light blue bg for active state base */
border-color: currentColor;
box-shadow: inset 0 1px 2px rgba(0,0,0,0.05);
}
/* Custom Colors for Buttons when active - darker text for readability on light */
#orbit-sim-wrapper #btn-circle.active { color: #16a34a; border-color: #16a34a; background-color: #dcfce7; }
#orbit-sim-wrapper #btn-ellipse.active { color: #0284c7; border-color: #0284c7; background-color: #e0f2fe; }
#orbit-sim-wrapper #btn-parabola.active { color: #d97706; border-color: #d97706; background-color: #fef3c7; }
#orbit-sim-wrapper #btn-hyperbola.active { color: #dc2626; border-color: #dc2626; background-color: #fee2e2; }
#orbit-sim-wrapper .btn-reset {
width: 100%;
background-color: #ef4444; /* Red-500 */
border-color: #dc2626;
color: white;
margin-top: 0.5rem;
}
#orbit-sim-wrapper .btn-reset:hover { background-color: #dc2626; }
/* CONTROLS */
#orbit-sim-wrapper .control-group {
margin-bottom: 0.75rem;
}
#orbit-sim-wrapper label {
display: flex;
justify-content: space-between;
font-size: 0.85rem;
margin-bottom: 0.25rem;
color: #475569;
}
#orbit-sim-wrapper input[type="range"] {
width: 100%;
accent-color: #0284c7; /* Sky-600 */
cursor: pointer;
height: 6px;
background: #e2e8f0;
border-radius: 3px;
appearance: none;
}
#orbit-sim-wrapper input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
background: #0284c7;
border-radius: 50%;
cursor: pointer;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
#orbit-sim-wrapper input[type="checkbox"] {
accent-color: #0284c7;
width: 16px;
height: 16px;
cursor: pointer;
}
#orbit-sim-wrapper .checkbox-row {
display: flex;
align-items: center;
margin-bottom: 0.5rem;
cursor: pointer;
}
#orbit-sim-wrapper .checkbox-row span {
margin-left: 0.5rem;
font-size: 0.9rem;
}
/* INFO OVERLAY */
#orbit-sim-wrapper .overlay-info {
position: absolute;
top: 15px;
left: 15px;
background: rgba(255, 255, 255, 0.9);
padding: 10px;
border-radius: 6px;
font-size: 0.85rem;
border: 1px solid #cbd5e1;
pointer-events: none;
color: #334155;
font-family: 'Segoe UI', sans-serif;
min-width: 150px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
#orbit-sim-wrapper .legend-item {
display: flex;
align-items: center;
margin-bottom: 4px;
font-size: 0.8rem;
font-weight: 600;
}
#orbit-sim-wrapper .legend-dot {
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
margin-right: 8px;
border: 1px solid rgba(0,0,0,0.1);
}
</style>
<div class="app-layout">
<!-- CONTROLS -->
<div class="controls-area">
<div>
<h2>Gravity Lab</h2>
<div class="subtitle">Multi-Orbit Comparison</div>
</div>
<!-- Orbit Toggles -->
<div>
<div class="section-header">Visible Orbits (Toggle)</div>
<div class="presets-grid">
<button id="btn-circle" class="active">
<span class="legend-dot" style="background-color: #16a34a;"></span> Circle
</button>
<button id="btn-ellipse" class="active">
<span class="legend-dot" style="background-color: #0284c7;"></span> Ellipse
</button>
<button id="btn-parabola" class="active">
<span class="legend-dot" style="background-color: #d97706;"></span> Parabola
</button>
<button id="btn-hyperbola" class="active">
<span class="legend-dot" style="background-color: #dc2626;"></span> Hyperbola
</button>
</div>
</div>
<!-- Parameters -->
<div>
<div class="section-header">Parameters</div>
<div class="control-group">
<label>
Simulation Speed
<span id="disp-speed">1.0x</span>
</label>
<input type="range" id="rng-speed" min="0" max="5.0" step="0.1" value="1.0">
</div>
<div class="control-group">
<label>
Zoom Level
<span id="disp-zoom">100%</span>
</label>
<input type="range" id="rng-zoom" min="0.1" max="3.0" step="0.1" value="1.0">
</div>
</div>
<!-- Visualization Options -->
<div>
<div class="section-header">Visuals</div>
<label class="checkbox-row">
<input type="checkbox" id="chk-vectors" checked>
<span>Show Vectors</span>
</label>
<label class="checkbox-row">
<input type="checkbox" id="chk-trail" checked>
<span>Show Trails</span>
</label>
</div>
<button id="btn-reset-sim" class="btn-reset">Reset All Positions</button>
</div>
<!-- CANVAS -->
<div class="canvas-area">
<canvas id="orbit-canvas"></canvas>
<div class="overlay-info">
<div class="section-header" style="margin-top:0; border-bottom: 1px solid #e2e8f0; color: #64748b;">Legend</div>
<div class="legend-item" style="color: #16a34a;"><span class="legend-dot" style="background-color: #16a34a;"></span> Circle (e=0)</div>
<div class="legend-item" style="color: #0284c7;"><span class="legend-dot" style="background-color: #0284c7;"></span> Ellipse (e=0.6)</div>
<div class="legend-item" style="color: #d97706;"><span class="legend-dot" style="background-color: #d97706;"></span> Parabola (e=1.0)</div>
<div class="legend-item" style="color: #dc2626;"><span class="legend-dot" style="background-color: #dc2626;"></span> Hyperbola (e=1.4)</div>
</div>
<div style="position:absolute; bottom:10px; right:10px; font-size:0.75rem; color:#64748b;">
Open orbits loop automatically
</div>
</div>
</div>
<script>
(function() {
// --- SETUP ---
const wrapper = document.getElementById('orbit-sim-wrapper');
const canvas = document.getElementById('orbit-canvas');
const ctx = canvas.getContext('2d');
// UI Refs
const btnCircle = document.getElementById('btn-circle');
const btnEllipse = document.getElementById('btn-ellipse');
const btnParabola = document.getElementById('btn-parabola');
const btnHyperbola = document.getElementById('btn-hyperbola');
const btnResetSim = document.getElementById('btn-reset-sim');
const rngSpeed = document.getElementById('rng-speed');
const rngZoom = document.getElementById('rng-zoom');
const dispSpeed = document.getElementById('disp-speed');
const dispZoom = document.getElementById('disp-zoom');
const chkVectors = document.getElementById('chk-vectors');
const chkTrail = document.getElementById('chk-trail');
// --- PHYSICS CONSTANTS ---
const GM = 5000;
const r_p = 150; // Periapsis distance
const RESET_DISTANCE = 1500; // Distance to reset open orbits
// --- SIMULATION STATE ---
let simState = {
timeScale: 1.0,
zoom: 1.0,
bodies: []
};
// --- INITIALIZATION ---
function createBody(type, ecc, color) {
const vp = Math.sqrt((GM / r_p) * (1 + ecc));
return {
type: type,
e: ecc,
color: color,
visible: true,
x: r_p,
y: 0,
vx: 0,
vy: -vp,
trail: []
};
}
function initSim() {
// Using darker colors for visibility on white background
simState.bodies = [
createBody('Circle', 0.0, '#16a34a'), // Green-600
createBody('Ellipse', 0.6, '#0284c7'), // Sky-600
createBody('Parabola', 1.0, '#d97706'), // Amber-600
createBody('Hyperbola', 1.4, '#dc2626') // Red-600
];
updateButtonState(btnCircle, 0);
updateButtonState(btnEllipse, 1);
updateButtonState(btnParabola, 2);
updateButtonState(btnHyperbola, 3);
}
function resetPositions() {
simState.bodies.forEach(b => {
const vp = Math.sqrt((GM / r_p) * (1 + b.e));
b.x = r_p;
b.y = 0;
b.vx = 0;
b.vy = -vp;
b.trail = [];
});
}
// --- PHYSICS ENGINE ---
function updatePhysics() {
if (simState.timeScale === 0) return;
const substeps = 8;
const dt = (0.1 * simState.timeScale) / substeps;
for (let i = 0; i < substeps; i++) {
simState.bodies.forEach(body => {
if (!body.visible) return;
const r2 = body.x * body.x + body.y * body.y;
const r = Math.sqrt(r2);
if (body.e >= 1.0 && r > RESET_DISTANCE) {
const movingAway = (body.x * body.vx + body.y * body.vy) > 0;
if (movingAway) {
body.x = body.x;
body.y = -body.y;
body.vx = -body.vx;
body.vy = body.vy;
body.trail = [];
return;
}
}
if (r < 15) return;
const accMag = GM / (r2 * r);
const ax = -accMag * body.x;
const ay = -accMag * body.y;
body.vx += ax * dt;
body.vy += ay * dt;
body.x += body.vx * dt;
body.y += body.vy * dt;
});
}
if (chkTrail.checked) {
simState.bodies.forEach(body => {
if (!body.visible) return;
if (body.trail.length === 0) {
body.trail.push({x: body.x, y: body.y});
} else {
const last = body.trail[body.trail.length - 1];
const d2 = (body.x - last.x)**2 + (body.y - last.y)**2;
if (d2 > 25) {
body.trail.push({x: body.x, y: body.y});
if (body.trail.length > 800) body.trail.shift();
}
}
});
} else {
simState.bodies.forEach(b => b.trail = []);
}
}
// --- DRAWING ---
function draw() {
const width = canvas.width;
const height = canvas.height;
const cx = width / 2;
const cy = height / 2;
// Calculate inverse zoom for consistent screen-size elements
const invZoom = 1 / simState.zoom;
ctx.clearRect(0, 0, width, height);
ctx.save();
ctx.translate(cx, cy);
ctx.scale(simState.zoom, simState.zoom);
// 1. Grid (Light gray for light theme)
ctx.strokeStyle = "#e2e8f0"; // Slate-200
ctx.lineWidth = 1 * invZoom; // Maintain 1px screen width
ctx.beginPath();
const gridSize = 100;
const limit = Math.max(width, height) / simState.zoom;
for(let i = -limit; i < limit; i+=gridSize) {
ctx.moveTo(i, -limit); ctx.lineTo(i, limit);
ctx.moveTo(-limit, i); ctx.lineTo(limit, i);
}
ctx.stroke();
// 2. Sun (Adapted for white background)
const grad = ctx.createRadialGradient(0, 0, 5, 0, 0, 30);
grad.addColorStop(0, "rgba(251, 146, 60, 0.8)"); // Orange-400
grad.addColorStop(1, "rgba(251, 146, 60, 0)");
ctx.fillStyle = grad;
ctx.beginPath();
// Sun glow scales with zoom but keeps some base size
ctx.arc(0, 0, 30, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = "#f97316"; // Orange-500
ctx.beginPath();
ctx.arc(0, 0, 8 * invZoom, 0, Math.PI*2); // Sun core constant screen size
ctx.fill();
// 3. Bodies
simState.bodies.forEach(body => {
if (!body.visible) return;
// Trail
if (body.trail.length > 1) {
ctx.strokeStyle = body.color;
ctx.globalAlpha = 0.6; // Slightly more visible opacity
ctx.lineWidth = 3 * invZoom; // Constant 3px screen width for trails
ctx.beginPath();
ctx.moveTo(body.trail[0].x, body.trail[0].y);
for (let i = 1; i < body.trail.length; i++) {
ctx.lineTo(body.trail[i].x, body.trail[i].y);
}
ctx.stroke();
ctx.globalAlpha = 1.0;
}
// Planet
ctx.fillStyle = body.color;
ctx.beginPath();
// Ensure constant screen size (~8px radius) regardless of zoom
ctx.arc(body.x, body.y, 8 * invZoom, 0, Math.PI*2);
ctx.fill();
// Vectors
if (chkVectors.checked) {
// Velocity
drawArrow(ctx, body.x, body.y, body.vx * 15, body.vy * 15, body.color, invZoom);
// Force (Gravity) - Dark gray arrow for contrast
const r = Math.hypot(body.x, body.y);
const fx = -(body.x/r) * 40;
const fy = -(body.y/r) * 40;
drawArrow(ctx, body.x, body.y, fx, fy, "rgba(100, 116, 139, 0.5)", invZoom); // Slate-500 semi-transparent
}
});
ctx.restore();
}
function drawArrow(ctx, x, y, dx, dy, color, scaleFactor) {
const len = Math.hypot(dx, dy);
// Don't draw if vector is too small on screen
if (len * (1/scaleFactor) < 3) return;
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.lineWidth = 2 * scaleFactor; // Constant screen width lines
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + dx, y + dy);
ctx.stroke();
const angle = Math.atan2(dy, dx);
// Head size scales to remain constant on screen
const head = 8 * scaleFactor;
ctx.beginPath();
ctx.moveTo(x + dx, y + dy);
ctx.lineTo(x + dx - head * Math.cos(angle - Math.PI/6), y + dy - head * Math.sin(angle - Math.PI/6));
ctx.lineTo(x + dx - head * Math.cos(angle + Math.PI/6), y + dy - head * Math.sin(angle + Math.PI/6));
ctx.fill();
}
// --- UI HELPERS ---
function toggleBody(index, btn) {
simState.bodies[index].visible = !simState.bodies[index].visible;
updateButtonState(btn, index);
}
function updateButtonState(btn, index) {
if (simState.bodies[index].visible) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
}
// --- LOOP ---
function loop() {
updatePhysics();
draw();
requestAnimationFrame(loop);
}
// --- EVENT LISTENERS ---
let resizeRequestId = null;
const resizeObserver = new ResizeObserver(() => {
if (!resizeRequestId) {
resizeRequestId = window.requestAnimationFrame(() => {
const rect = canvas.parentElement.getBoundingClientRect();
canvas.width = rect.width;
canvas.height = rect.height;
resizeRequestId = null;
});
}
});
resizeObserver.observe(canvas.parentElement);
btnCircle.addEventListener('click', () => toggleBody(0, btnCircle));
btnEllipse.addEventListener('click', () => toggleBody(1, btnEllipse));
btnParabola.addEventListener('click', () => toggleBody(2, btnParabola));
btnHyperbola.addEventListener('click', () => toggleBody(3, btnHyperbola));
btnResetSim.addEventListener('click', resetPositions);
rngSpeed.addEventListener('input', (e) => {
simState.timeScale = parseFloat(e.target.value);
dispSpeed.textContent = simState.timeScale.toFixed(1) + "x";
});
rngZoom.addEventListener('input', (e) => {
simState.zoom = parseFloat(e.target.value);
dispZoom.textContent = Math.round(simState.zoom * 100) + "%";
});
// INIT
initSim();
loop();
})();
</script>
</div>
<!-- ORBITAL MECHANICS SIMULATOR END -->
```
### Escape Velocity
Escape velocity is the minimum velocity required for an object to escape the gravitational pull of a planet or celestial body without further propulsion. It is given by:
$$
v_\text{escape} = \sqrt{2G \frac{M}{R}}
$$
where $M$ is the mass of the celestial body and $R$ is its radius.
### Numerical Example: Escape Velocity for Earth
- Mass of Earth ($M_\text{Earth}$): $5.972 \times 10^{24} \ \mathrm{kg}$
- Radius of Earth ($R_\text{Earth}$): $6.371 \times 10^6 \ \mathrm{m}$
```{python}
# Constants
G = 6.674e-11 # Gravitational constant (N m^2/kg^2)
M_earth = 5.972e24 # Mass of Earth (kg)
R_earth = 6.371e6 # Radius of Earth (m)
# Escape velocity calculation
v_escape = np.sqrt(2 * G * M_earth / R_earth)
print(f"Escape velocity for Earth: {v_escape/1000:.1f} km/s")
```
```{=html}
<!-- NEWTON'S CANNON SIMULATOR START -->
<div id="newton-cannon-wrapper">
<style>
/* SCOPED CSS - LIGHT THEME */
#newton-cannon-wrapper {
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
background-color: #ffffff;
color: #334155;
padding: 0;
margin: 0;
box-sizing: border-box;
width: 100%;
height: auto;
min-height: 650px;
display: flex;
flex-direction: column;
border: 1px solid #cbd5e1;
border-radius: 8px;
overflow: hidden;
position: relative;
}
#newton-cannon-wrapper * {
box-sizing: border-box;
}
/* LAYOUT */
#newton-cannon-wrapper .app-layout {
display: flex;
flex-direction: column;
min-height: 650px;
flex: 1;
}
@media (min-width: 900px) {
#newton-cannon-wrapper .app-layout {
flex-direction: row;
}
}
/* CONTROLS AREA (LEFT) */
#newton-cannon-wrapper .controls-area {
width: 100%;
background-color: #f8fafc;
border-right: 1px solid #cbd5e1;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
overflow-y: auto;
order: 1;
z-index: 2;
}
@media (min-width: 900px) {
#newton-cannon-wrapper .controls-area {
width: 340px;
flex-shrink: 0;
height: auto;
max-height: none;
}
}
/* CANVAS AREA (RIGHT) */
#newton-cannon-wrapper .canvas-area {
flex: 1;
position: relative;
background-color: #ffffff;
overflow: hidden;
min-height: 400px;
cursor: default;
order: 2;
}
#newton-cannon-wrapper canvas {
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
/* TYPOGRAPHY & UI ELEMENTS */
#newton-cannon-wrapper h2 {
margin: 0 0 0.25rem 0;
font-size: 1.5rem;
color: #0f172a;
font-weight: 700;
}
#newton-cannon-wrapper .subtitle {
font-size: 0.85rem;
color: #64748b;
margin-bottom: 1rem;
}
#newton-cannon-wrapper .section-header {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #475569;
font-weight: 700;
margin-bottom: 0.5rem;
border-bottom: 1px solid #e2e8f0;
padding-bottom: 0.25rem;
}
/* PRESET BUTTONS */
#newton-cannon-wrapper .presets-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
margin-bottom: 1rem;
}
#newton-cannon-wrapper button {
padding: 0.6rem;
border: 1px solid #cbd5e1;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
font-size: 0.85rem;
background-color: #ffffff;
color: #334155;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
}
#newton-cannon-wrapper button:hover {
background-color: #f1f5f9;
border-color: #94a3b8;
}
#newton-cannon-wrapper button.active {
background-color: #eff6ff;
border-color: #3b82f6;
color: #1d4ed8;
box-shadow: inset 0 1px 2px rgba(0,0,0,0.05);
}
#newton-cannon-wrapper .btn-fire {
width: 100%;
background-color: #dc2626; /* Red fire button */
border-color: #b91c1c;
color: white;
margin-top: 0.5rem;
font-size: 1rem;
text-transform: uppercase;
letter-spacing: 1px;
}
#newton-cannon-wrapper .btn-fire:hover { background-color: #b91c1c; }
/* SLIDERS */
#newton-cannon-wrapper .control-group {
margin-bottom: 1rem;
}
#newton-cannon-wrapper label {
display: flex;
justify-content: space-between;
font-size: 0.85rem;
margin-bottom: 0.25rem;
color: #475569;
font-weight: 500;
}
#newton-cannon-wrapper input[type="range"] {
width: 100%;
accent-color: #0f172a;
cursor: pointer;
height: 6px;
background: #e2e8f0;
border-radius: 3px;
appearance: none;
}
#newton-cannon-wrapper input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 18px;
height: 18px;
background: #0f172a;
border-radius: 50%;
cursor: pointer;
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
margin-top: -6px; /* chrome fix */
}
/* Slider track */
#newton-cannon-wrapper input[type="range"]::-webkit-slider-runnable-track {
width: 100%;
height: 6px;
cursor: pointer;
background: #e2e8f0;
border-radius: 3px;
}
/* SLIDER MARKERS */
#newton-cannon-wrapper .slider-markers {
display: flex;
justify-content: space-between;
font-size: 0.7rem;
color: #94a3b8;
margin-top: 4px;
position: relative;
height: 15px;
}
#newton-cannon-wrapper .marker {
position: absolute;
transform: translateX(-50%);
text-align: center;
}
/* INFO OVERLAY */
#newton-cannon-wrapper .overlay-info {
position: absolute;
top: 15px;
left: 15px;
background: rgba(255, 255, 255, 0.95);
padding: 12px;
border-radius: 6px;
font-size: 0.85rem;
border: 1px solid #cbd5e1;
pointer-events: none;
color: #334155;
font-family: 'Segoe UI', sans-serif;
min-width: 160px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
#newton-cannon-wrapper .status-indicator {
font-weight: bold;
margin-top: 5px;
padding-top: 5px;
border-top: 1px solid #e2e8f0;
}
</style>
<div class="app-layout">
<!-- CONTROL PANEL -->
<div class="controls-area">
<div>
<h2>Newton's Cannon</h2>
<div class="subtitle">Orbital Thought Experiment</div>
</div>
<!-- Launch Parameters -->
<div>
<div class="section-header">Launch Velocity Control</div>
<div class="control-group">
<label>
Velocity (v)
<span id="disp-velocity" style="color:#0f172a; font-weight:bold;">---</span>
</label>
<!--
Scale updated:
Min: 0.85 * v1 (Sub-orbital)
Step: 0.001 (High precision)
Max: 1.6 * v1
-->
<input type="range" id="rng-velocity" min="0.85" max="1.6" step="0.001" value="0.85">
<div class="slider-markers">
<div class="marker" style="left: 0%">0.85v₁</div>
<!-- 1.0 is at (1.0 - 0.85) / 0.75 = 20% -->
<div class="marker" style="left: 20%">v₁</div>
<!-- 1.414 is at (1.414 - 0.85) / 0.75 = 75.2% -->
<div class="marker" style="left: 75.2%">v₂</div>
</div>
</div>
</div>
<!-- Presets -->
<div>
<div class="section-header">Quick Presets</div>
<div class="presets-grid">
<button id="btn-fall">Fall<br>(v = 0.9)</button>
<button id="btn-circle">Circular Orbit<br>(v = v₁)</button>
<button id="btn-ellipse">Ellipse<br>(v₁ < v < v₂)</button>
<button id="btn-escape">Escape<br>(v ≥ v₂)</button>
</div>
</div>
<div class="control-group">
<div class="section-header">Simulation Parameters</div>
<label>
Time Speed
<span id="disp-time">1.0x</span>
</label>
<input type="range" id="rng-timescale" min="0.1" max="3.0" step="0.1" value="1.0">
<div style="height: 10px;"></div>
<label>
Camera Zoom
<span id="disp-zoom">100%</span>
</label>
<!-- Scale range: 0.1 (far) to 2.5 (close up) -->
<input type="range" id="rng-zoom" min="0.1" max="2.5" step="0.1" value="1.5">
</div>
<button id="btn-fire" class="btn-fire">FIRE / RESET</button>
<div style="margin-top: 1rem; font-size: 0.8rem; color: #64748b; line-height: 1.4;">
<strong>Legend:</strong><br>
v₁ = 1st Cosmic Velocity (Circular)<br>
v₂ = 2nd Cosmic Velocity (Escape)<br>
Tower height is 10% of Earth's radius.
</div>
</div>
<!-- VISUALIZATION -->
<div class="canvas-area">
<canvas id="cannon-canvas"></canvas>
<div class="overlay-info">
<div>Altitude (h): <span id="info-h" style="font-weight:bold;">---</span> km</div>
<div>Velocity (v): <span id="info-v">---</span> km/s</div>
<div id="info-status" class="status-indicator" style="color: #64748b;">Ready to fire</div>
</div>
</div>
</div>
<script>
(function() {
// --- CONFIGURATION ---
const wrapper = document.getElementById('newton-cannon-wrapper');
const canvas = document.getElementById('cannon-canvas');
const ctx = canvas.getContext('2d');
// UI Elements
const rngVelocity = document.getElementById('rng-velocity');
const rngTimeScale = document.getElementById('rng-timescale');
const rngZoom = document.getElementById('rng-zoom');
const dispVelocity = document.getElementById('disp-velocity');
const dispTime = document.getElementById('disp-time');
const dispZoom = document.getElementById('disp-zoom');
const btnFire = document.getElementById('btn-fire');
const btnFall = document.getElementById('btn-fall');
const btnCircle = document.getElementById('btn-circle');
const btnEllipse = document.getElementById('btn-ellipse');
const btnEscape = document.getElementById('btn-escape');
const infoH = document.getElementById('info-h');
const infoV = document.getElementById('info-v');
const infoStatus = document.getElementById('info-status');
// --- PHYSICS & GEOMETRY CONSTANTS ---
const EARTH_RADIUS = 80; // pixels
const TOWER_HEIGHT = 8; // pixels (10% of radius)
const LAUNCH_RADIUS = EARTH_RADIUS + TOWER_HEIGHT;
// V1 (Circular velocity) setup
const V1_BASE = 4.0;
const GM = V1_BASE * V1_BASE * LAUNCH_RADIUS;
const V2_BASE = V1_BASE * Math.sqrt(2); // Escape velocity
// Simulation State
let state = {
running: false,
crashed: false,
x: 0,
y: -LAUNCH_RADIUS, // Start at North Pole
vx: 0,
vy: 0,
trail: [],
zoom: 1.5, // Default zoom (manual control)
simSpeed: 1.0
};
let animationId;
// --- LOGIC ---
function initShot() {
const multiplier = parseFloat(rngVelocity.value);
const startV = V1_BASE * multiplier;
state.running = true;
state.crashed = false;
state.x = 0;
state.y = -LAUNCH_RADIUS;
// Horizontal shot to the right (+x)
state.vx = startV;
state.vy = 0;
state.trail = [];
state.trail.push({x: state.x, y: state.y});
infoStatus.textContent = "In flight...";
infoStatus.style.color = "#2563eb"; // Blue
btnFire.textContent = "RESET";
}
function resetSim() {
state.running = false;
state.crashed = false;
state.x = 0;
state.y = -LAUNCH_RADIUS;
state.trail = [];
infoStatus.textContent = "Ready";
infoStatus.style.color = "#64748b";
btnFire.textContent = "FIRE";
draw();
}
function updatePhysics() {
if (!state.running || state.crashed) return;
// Substeps for precision near surface
const substeps = 4;
const dt = (0.5 * state.simSpeed) / substeps;
for(let i=0; i<substeps; i++) {
const r2 = state.x*state.x + state.y*state.y;
const r = Math.sqrt(r2);
// 1. Collision Detection
if (r <= EARTH_RADIUS) {
state.crashed = true;
state.running = false;
infoStatus.textContent = "IMPACT";
infoStatus.style.color = "#dc2626"; // Red
// Move exactly to surface for visuals
const factor = EARTH_RADIUS / r;
state.x *= factor;
state.y *= factor;
break;
}
// 2. Gravity (a = -GM/r^3 * r_vec)
const acc = GM / (r2 * r);
const ax = -acc * state.x;
const ay = -acc * state.y;
// 3. Integration (Symplectic Euler)
state.vx += ax * dt;
state.vy += ay * dt;
state.x += state.vx * dt;
state.y += state.vy * dt;
}
// Trail Update
if (state.trail.length === 0) {
state.trail.push({x: state.x, y: state.y});
} else {
const last = state.trail[state.trail.length-1];
const distSq = (state.x - last.x)**2 + (state.y - last.y)**2;
if (distSq > 10) {
state.trail.push({x: state.x, y: state.y});
if (state.trail.length > 2000) state.trail.shift();
}
}
}
function updateInfo() {
if (!state.running && !state.crashed && btnFire.textContent === "FIRE") {
// Show preset parameters
const mult = parseFloat(rngVelocity.value);
const v = V1_BASE * mult;
const realV = (mult * 7.91).toFixed(2); // 7.91 km/s is approx v1 for Earth
infoV.innerText = `${realV} km/s`;
infoH.innerText = "---";
return;
}
const r = Math.sqrt(state.x*state.x + state.y*state.y);
const vCurrent = Math.sqrt(state.vx*state.vx + state.vy*state.vy);
// Real units conversion
// R_earth ~6371 km
const altitudePx = r - EARTH_RADIUS;
const altitudeKm = (altitudePx / EARTH_RADIUS) * 6371;
// Velocity: v1 = 7.91 km/s
const realV = (vCurrent / V1_BASE) * 7.91;
infoH.innerText = Math.max(0, altitudeKm).toFixed(0);
infoV.innerText = realV.toFixed(2);
}
// --- DRAWING ---
function draw() {
const width = canvas.width;
const height = canvas.height;
const cx = width / 2;
const cy = height / 2;
ctx.clearRect(0, 0, width, height);
// Manual Zoom Control
// baseScale ensures Earth fits nicely at zoom 1.0
const minDim = Math.min(width, height);
const baseScale = minDim / (3.5 * EARTH_RADIUS);
const scale = baseScale * state.zoom;
ctx.save();
ctx.translate(cx, cy);
ctx.scale(scale, scale);
// 1. Earth
ctx.fillStyle = "#e2e8f0"; // Atmosphere halo
ctx.beginPath();
ctx.arc(0, 0, EARTH_RADIUS * 1.1, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = "#3b82f6"; // Earth Blue
ctx.beginPath();
ctx.arc(0, 0, EARTH_RADIUS, 0, Math.PI*2);
ctx.fill();
// Earth Grid
ctx.strokeStyle = "rgba(255,255,255,0.3)";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(-EARTH_RADIUS, 0); ctx.lineTo(EARTH_RADIUS, 0);
ctx.moveTo(0, -EARTH_RADIUS); ctx.lineTo(0, EARTH_RADIUS);
ctx.stroke();
// 2. Tower
ctx.fillStyle = "#64748b"; // Concrete gray
const tW = 4; // Slimmer tower
ctx.fillRect(-tW/2, -LAUNCH_RADIUS, tW, TOWER_HEIGHT);
// Cannon base
ctx.fillStyle = "#1e293b";
ctx.beginPath();
ctx.arc(0, -LAUNCH_RADIUS, tW, 0, Math.PI*2);
ctx.fill();
// 3. Trajectory
if (state.trail.length > 1) {
ctx.strokeStyle = "#ef4444"; // Red trail
ctx.lineWidth = 2 / scale; // Constant screen width
ctx.beginPath();
ctx.moveTo(state.trail[0].x, state.trail[0].y);
for (let i = 1; i < state.trail.length; i++) {
ctx.lineTo(state.trail[i].x, state.trail[i].y);
}
ctx.stroke();
}
// 4. Projectile
if (state.running || state.crashed) {
ctx.fillStyle = "#dc2626";
ctx.beginPath();
ctx.arc(state.x, state.y, 5 / scale, 0, Math.PI*2);
ctx.fill();
}
// Impact marker
if (state.crashed) {
ctx.fillStyle = "yellow";
ctx.font = `bold ${14/scale}px Arial`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("💥", state.x, state.y);
}
ctx.restore();
}
function loop() {
updatePhysics();
updateInfo();
draw();
animationId = requestAnimationFrame(loop);
}
// --- EVENT HANDLERS ---
// Resize
let resizeRequestId = null;
const resizeObserver = new ResizeObserver(() => {
if (!resizeRequestId) {
resizeRequestId = window.requestAnimationFrame(() => {
const rect = canvas.parentElement.getBoundingClientRect();
canvas.width = rect.width;
canvas.height = rect.height;
if (!state.running) draw();
resizeRequestId = null;
});
}
});
resizeObserver.observe(canvas.parentElement);
// Sliders
rngVelocity.addEventListener('input', (e) => {
const val = parseFloat(e.target.value);
let label = "";
// Increased precision display to 3 decimal places
if (val < 1.0) label = `v < v₁ (${val.toFixed(3)})`;
else if (Math.abs(val - 1.0) < 0.002) label = `v ≈ v₁ (Orbit)`;
else if (val < 1.414) label = `v₁ < v < v₂ (${val.toFixed(3)})`;
else label = `v > v₂ (Escape)`;
dispVelocity.textContent = label;
if (!state.running) updateInfo();
});
rngTimeScale.addEventListener('input', (e) => {
state.simSpeed = parseFloat(e.target.value);
dispTime.textContent = state.simSpeed.toFixed(1) + "x";
});
rngZoom.addEventListener('input', (e) => {
state.zoom = parseFloat(e.target.value);
dispZoom.textContent = Math.round(state.zoom * 100) + "%";
if (!state.running) draw();
});
// Buttons
btnFire.addEventListener('click', () => {
if (state.running || state.crashed) {
resetSim();
} else {
initShot();
}
});
// Presets
function setPreset(val) {
rngVelocity.value = val;
rngVelocity.dispatchEvent(new Event('input'));
resetSim();
}
// Updated Fall preset to be within new range (0.85 - 1.6)
btnFall.addEventListener('click', () => setPreset(0.9));
btnCircle.addEventListener('click', () => setPreset(1.0));
btnEllipse.addEventListener('click', () => setPreset(1.2));
btnEscape.addEventListener('click', () => setPreset(1.45));
// Start
resetSim();
rngVelocity.dispatchEvent(new Event('input')); // Init label
loop();
})();
</script>
</div>
<!-- NEWTON'S CANNON SIMULATOR END -->
``
### Gravitational Potential Energy
The gravitational potential energy $U$ of a two-body system is given by:
$$
U = -G \frac{m_1 m_2}{r}
$$
This energy is negative because the gravitational force is attractive, and the potential energy is zero at infinite separation.
### Applications of Universal Gravitation
1. **Planetary Orbits:** Predicting the motion of planets and moons in their orbits.
2. **Space Travel:** Calculating escape velocities and transfer orbits for spacecraft.
3. **Tidal Effects:** Understanding the gravitational interaction between Earth and Moon causing ocean tides.
4. **Astrophysics:** Studying gravitational interactions in galaxies and black holes.
### Small body around the Earth
```{python}
import numpy as np
import matplotlib.pyplot as plt
# Parametry
G = 6.674e-11 # Stała grawitacyjna (m^3/kg/s^2)
M = 5.972e24 # Masa większego ciała, np. Ziemi (kg)
m = 1000 # Masa mniejszego ciała, np. satelity (kg)
# Warunki początkowe
r0 = 7e6 # Początkowa odległość od środka masy większego ciała (m)
v0 = 5200 # Początkowa prędkość orbitalna (m/s)
theta0 = 0 # Początkowy kąt w radianach
# Czas
T = 6000 # Czas symulacji (s)
N = 100_000 # Liczba kroków czasowych
dt = T / N
# Tablice dla pozycji i prędkości
x = np.zeros(N)
y = np.zeros(N)
vx = np.zeros(N)
vy = np.zeros(N)
# Warunki początkowe
x[0] = r0 * np.cos(theta0)
y[0] = r0 * np.sin(theta0)
vx[0] = -v0 * np.sin(theta0)
vy[0] = v0 * np.cos(theta0)
# Numeryczna integracja (metoda Eulera)
for i in range(1, N):
r = np.sqrt(x[i - 1]**2 + y[i - 1]**2) # Odległość od środka masy
ax = -G * M * x[i - 1] / r**3 # Przyspieszenie w osi x
ay = -G * M * y[i - 1] / r**3 # Przyspieszenie w osi y
# Aktualizacja prędkości
vx[i] = vx[i - 1] + ax * dt
vy[i] = vy[i - 1] + ay * dt
# Aktualizacja pozycji
x[i] = x[i - 1] + vx[i - 1] * dt
y[i] = y[i - 1] + vy[i - 1] * dt
# Wizualizacja trajektorii
plt.figure(figsize=(8, 8))
plt.plot(x, y, label="Trajektoria")
plt.plot(0, 0, 'ro', label="Centralne ciało (np. Ziemia)")
plt.xlabel("x (m)")
plt.ylabel("y (m)")
plt.title("Trajektoria orbitalna małego ciała")
plt.legend(loc='lower right')
plt.grid()
plt.axis('equal')
plt.show()
```
### Earth-Moon System with small body
```{python}
import numpy as np
import matplotlib.pyplot as plt
# Parameters
G = 6.674e-11 # Gravitational constant (m^3/kg/s^2)
M_earth = 5.972e24 # Mass of Earth (kg)
M_moon = 7.348e22 # Mass of Moon (kg)
R_earth_moon = 3.844e8 # Distance between Earth and Moon (m)
m_probe = 1000 # Mass of the probe (kg)
# Initial positions
x_earth, y_earth = 0, 0
x_moon, y_moon = R_earth_moon, 0
# Launch parameters
launch_angle = 5 # Launch angle in degrees (relative to +x-axis)
initial_speed = 1500 # Initial speed of the probe (m/s)
# Convert launch angle to radians
launch_angle_rad = np.radians(launch_angle)
# Probe initial position and velocity
x_probe, y_probe = R_earth_moon / 2, 0 # Start halfway between Earth and Moon
vx_probe = initial_speed * np.cos(launch_angle_rad)
vy_probe = initial_speed * np.sin(launch_angle_rad)
# Time settings
T =15 * 24 * 3600 # Total simulation time (s)
N = 500000 # Number of time steps
dt = T / N # Time step (s)
# Arrays to store positions
x_positions = []
y_positions = []
# Simulation loop
for i in range(N):
# Distance between probe and Earth
r_earth = np.sqrt((x_probe - x_earth)**2 + (y_probe - y_earth)**2)
# Distance between probe and Moon
r_moon = np.sqrt((x_probe - x_moon)**2 + (y_probe - y_moon)**2)
# Gravitational accelerations
ax_earth = -G * M_earth * (x_probe - x_earth) / r_earth**3
ay_earth = -G * M_earth * (y_probe - y_earth) / r_earth**3
ax_moon = -G * M_moon * (x_probe - x_moon) / r_moon**3
ay_moon = -G * M_moon * (y_probe - y_moon) / r_moon**3
# Total acceleration
ax = ax_earth + ax_moon
ay = ay_earth + ay_moon
# Update velocities
vx_probe += ax * dt
vy_probe += ay * dt
# Update positions
x_probe += vx_probe * dt
y_probe += vy_probe * dt
# Store positions
x_positions.append(x_probe)
y_positions.append(y_probe)
# Convert to arrays
x_positions = np.array(x_positions)
y_positions = np.array(y_positions)
# Plot the results
plt.figure(figsize=(8, 8))
plt.plot(x_positions, y_positions, label="Probe Trajectory")
plt.plot(x_earth, y_earth, 'bo', label="Earth", markersize=10)
plt.plot(x_moon, y_moon, 'go', label="Moon", markersize=7)
plt.xlabel("x (m)")
plt.ylabel("y (m)")
plt.title("Trajectory of the Probe in the Earth-Moon System")
plt.legend()
plt.grid()
plt.axis('equal')
plt.show()
```
## Work and Kinetic Energy Relation
### Work
In three-dimensional motion, the work done by a force $\mathbf{F}$ acting on an object as it moves along a displacement $d\mathbf{r}$ is given by the **dot product**:
$$
dW = \mathbf{F} \cdot d\mathbf{r}
$$
Expanding in component form:
$$
dW = F_x dx + F_y dy + F_z dz
$$
Using **Newton’s second law** $\mathbf{F} = m \mathbf{a}$:
$$
dW = (m a_x dx + m a_y dy + m a_z dz)
$$
Since acceleration is the derivative of velocity:
$$
a_x = \frac{dv_x}{dt}, \quad a_y = \frac{dv_y}{dt}, \quad a_z = \frac{dv_z}{dt}
$$
And velocity components are:
$$
v_x = \frac{dx}{dt}, \quad v_y = \frac{dy}{dt}, \quad v_z = \frac{dz}{dt}
$$
Rewriting work in terms of velocity:
$$
dW = m v_x dv_x + m v_y dv_y + m v_z dv_z
$$
### Kinetic Energy
To determine the total work done from an initial velocity $\mathbf{v}_1 = (v_{1x}, v_{1y}, v_{1z})$ to a final velocity $\mathbf{v}_2 = (v_{2x}, v_{2y}, v_{2z})$:
$$
W = \int_{v_{1x}}^{v_{2x}} m v_x dv_x + \int_{v_{1y}}^{v_{2y}} m v_y dv_y + \int_{v_{1z}}^{v_{2z}} m v_z dv_z
$$
Solving each integral:
$$
W = m \left( \frac{v_{2x}^2}{2} - \frac{v_{1x}^2}{2} \right) + m \left( \frac{v_{2y}^2}{2} - \frac{v_{1y}^2}{2} \right) + m \left( \frac{v_{2z}^2}{2} - \frac{v_{1z}^2}{2} \right)
$$
Rewriting:
$$
W = \frac{1}{2} m \left( v_{2x}^2 + v_{2y}^2 + v_{2z}^2 \right) - \frac{1}{2} m \left( v_{1x}^2 + v_{1y}^2 + v_{1z}^2 \right)
$$
Since kinetic energy in 3D is defined as:
$$
KE = \frac{1}{2} m (v_x^2 + v_y^2 + v_z^2)
$$
we obtain:
$$
W = KE_2 - KE_1 = \Delta KE
$$
### Energy Conservation
In many physical situations, forces can be described by a **potential energy function**. A force $\mathbf{F}$ is called **conservative** if the work it does on an object moving from point $A$ to point $B$ is **independent of the path taken** and depends only on the initial and final positions. This allows us to define a scalar function, the **potential energy** $U$, such that:
$$
\mathbf{F} = - \nabla U
$$
This relation means that the force is the negative gradient of the potential energy function.
#### Examples of Potential Forces
There are several important cases of conservative forces where the **work-energy theorem** and **energy conservation** provide quick solutions to problems:
1. **Constant Force (Projectile Motion)**:
In uniform gravitational fields (e.g., near Earth's surface), the force is constant:
$$
\mathbf{F} = -mg \hat{j}
$$
The associated potential energy is given by:
$$
U = mg y
$$
This simplifies projectile motion calculations using energy conservation.
2. **Harmonic Oscillator (Spring Force)**:
A restoring force proportional to displacement, such as Hooke’s Law:
$$
\mathbf{F} = - k x \hat{i}
$$
The corresponding potential energy function is:
$$
U = \frac{1}{2} k x^2
$$
This is essential in analyzing oscillatory motion.
3. **Gravitational Force (Newtonian Gravity)**:
In a central gravitational field, the force obeys an inverse-square law:
$$
\mathbf{F} = - \frac{G m M}{r^2} \hat{r}
$$
The potential energy function is:
$$
U = -\frac{G m M}{r}
$$
This form is crucial for planetary motion and orbital mechanics.
### Energy Conservation
Since conservative forces allow potential energy to be defined, the **total mechanical energy** is conserved:
$$
E = KE + U = \text{constant}
$$
This principle allows for elegant solutions to motion problems without explicitly solving Newton’s second law.
#### Gravity example
$$
\frac{m v_1^2}{2}-\frac{G m M}{r_1}
=
\frac{m v_2^2}{2}-\frac{G m M}{r_2}
$$